NeteaseCloudMusicApi
Advanced tools
Comparing version 3.0.0 to 3.0.1
156
app.js
@@ -0,107 +1,79 @@ | ||
const fs = require('fs') | ||
const path = require('path') | ||
const express = require('express') | ||
const apicache = require('apicache') | ||
const path = require('path') | ||
const fs = require('fs') | ||
const app = express() | ||
let cache = apicache.middleware | ||
const { exec } = require('child_process'); | ||
const request = require('./util/request') | ||
const package = require('./package.json') | ||
const exec = require('child_process').exec | ||
const cache = require('apicache').middleware | ||
// version check | ||
exec('npm info NeteaseCloudMusicApi version', (err, stdout, stderr) => { | ||
if (err) { | ||
console.error(err); | ||
return; | ||
} | ||
const onlinePackageVersion = stdout.trim(); | ||
const package = require('./package.json') | ||
if (package.version < onlinePackageVersion) { | ||
console.log( | ||
'最新版:Version:' + | ||
onlinePackageVersion + | ||
',当前版本:' + | ||
package.version + | ||
',请及时更新' | ||
) | ||
} | ||
if(!err){ | ||
let version = stdout.trim() | ||
if(package.version < version){ | ||
console.log(`最新版本: ${version}, 当前版本: ${package.version}, 请及时更新`) | ||
} | ||
} | ||
}) | ||
// 跨域设置 | ||
app.all('*', function(req, res, next) { | ||
if (req.path !== '/' && !req.path.includes('.')) { | ||
res.header('Access-Control-Allow-Credentials', true) | ||
// 这里获取 origin 请求头 而不是用 * | ||
res.header('Access-Control-Allow-Origin', req.headers['origin'] || '*') | ||
res.header('Access-Control-Allow-Headers', 'X-Requested-With') | ||
res.header('Access-Control-Allow-Methods', 'PUT,POST,GET,DELETE,OPTIONS') | ||
res.header('Content-Type', 'application/json;charset=utf-8') | ||
} | ||
next() | ||
}) | ||
const app = express() | ||
const onlyStatus200 = (req, res) => res.statusCode === 200 | ||
app.use(cache('2 minutes', onlyStatus200)) | ||
app.use(express.static(path.resolve(__dirname, 'public'))) | ||
// 补全缺失的cookie | ||
const { completeCookie } = require('./util/init') | ||
app.use(function(req, res, next) { | ||
let cookie = completeCookie(req.headers.cookie) | ||
req.headers.cookie = cookie.map(x => x[0]).concat(req.headers.cookie || []).join('; ') | ||
res.append('Set-Cookie', cookie.map(x => (x.concat('Path=/').join('; ')))) | ||
next() | ||
// CORS | ||
app.use((req, res, next) => { | ||
if(req.path !== '/' && !req.path.includes('.')){ | ||
res.header({ | ||
'Access-Control-Allow-Credentials': true, | ||
'Access-Control-Allow-Origin': req.headers.origin || '*', | ||
'Access-Control-Allow-Headers': 'X-Requested-With', | ||
'Access-Control-Allow-Methods': 'PUT,POST,GET,DELETE,OPTIONS', | ||
'Content-Type': 'application/json; charset=utf-8' | ||
}) | ||
} | ||
next() | ||
}) | ||
// cookie parser | ||
app.use(function(req, res, next) { | ||
req.cookies = {}, (req.headers.cookie || '').split(/\s*;\s*/).forEach(pair => { | ||
let crack = pair.indexOf('=') | ||
if(crack < 1 || crack == pair.length - 1) return | ||
req.cookies[decodeURIComponent(pair.slice(0, crack)).trim()] = decodeURIComponent(pair.slice(crack + 1)).trim() | ||
}) | ||
next() | ||
app.use((req, res, next) => { | ||
req.cookies = {}, (req.headers.cookie || '').split(/\s*;\s*/).forEach(pair => { | ||
let crack = pair.indexOf('=') | ||
if(crack < 1 || crack == pair.length - 1) return | ||
req.cookies[decodeURIComponent(pair.slice(0, crack)).trim()] = decodeURIComponent(pair.slice(crack + 1)).trim() | ||
}) | ||
next() | ||
}) | ||
app.use(function(req, res, next) { | ||
const proxy = req.query.proxy | ||
if (proxy) { | ||
req.headers.cookie += `__proxy__${proxy}` | ||
} | ||
next() | ||
}) | ||
// cache | ||
app.use(cache('2 minutes', ((req, res) => res.statusCode === 200))) | ||
// 因为这几个文件对外所注册的路由 和 其他文件对外注册的路由规则不一样, 所以专门写个MAP对这些文件做特殊处理 | ||
const UnusualRouteFileMap = { | ||
// key 为文件名, value 为对外注册的路由 | ||
'daily_signin.js': '/daily_signin', | ||
'fm_trash.js': '/fm_trash', | ||
'personal_fm.js': '/personal_fm' | ||
// static | ||
app.use(express.static(path.join(__dirname, 'public'))) | ||
// router | ||
const special = { | ||
'daily_signin.js': '/daily_signin', | ||
'fm_trash.js': '/fm_trash', | ||
'personal_fm.js': '/personal_fm' | ||
} | ||
// 改写router为module | ||
const requestMod = require('./util/request') | ||
let dev = express() | ||
fs.readdirSync(path.join(__dirname, 'module')) | ||
.reverse() | ||
.forEach(file => { | ||
if (!(/\.js$/i.test(file))) return | ||
let route = (file in UnusualRouteFileMap) ? UnusualRouteFileMap[file] : '/' + file.replace(/\.js$/i, '').replace(/_/g, '/') | ||
let question = require(path.join(__dirname, 'module', file)) | ||
dev.use(route, (req, res) => { | ||
let query = {...req.query, cookie: req.cookies} | ||
question(query, requestMod) | ||
.then(answer => { | ||
console.log('[OK]', decodeURIComponent(req.originalUrl)) | ||
res.append('Set-Cookie', answer.cookie) | ||
res.status(answer.status).send(answer.body) | ||
fs.readdirSync(path.join(__dirname, 'module')).reverse().forEach(file => { | ||
if(!(/\.js$/i.test(file))) return | ||
let route = (file in special) ? special[file] : '/' + file.replace(/\.js$/i, '').replace(/_/g, '/') | ||
let question = require(path.join(__dirname, 'module', file)) | ||
app.use(route, (req, res) => { | ||
let query = {...req.query, ...req.body, cookie: req.cookies} | ||
question(query, request) | ||
.then(answer => { | ||
console.log('[OK]', decodeURIComponent(req.originalUrl)) | ||
res.append('Set-Cookie', answer.cookie) | ||
res.status(answer.status).send(answer.body) | ||
}) | ||
.catch(answer => { | ||
console.log('[ERR]', decodeURIComponent(req.originalUrl)) | ||
if(answer.body.code =='301') answer.body.msg = '需要登录' | ||
res.append('Set-Cookie', answer.cookie) | ||
res.status(answer.status).send(answer.body) | ||
}) | ||
}) | ||
.catch(answer => { | ||
console.log('[ERR]', decodeURIComponent(req.originalUrl)) | ||
res.append('Set-Cookie', answer.cookie) | ||
res.status(answer.status).send(answer.body) | ||
}) | ||
}) | ||
}) | ||
app.use('/', dev) | ||
@@ -111,5 +83,5 @@ const port = process.env.PORT || 3000 | ||
app.server = app.listen(port, () => { | ||
console.log(`server running @ http://localhost:${port}`) | ||
console.log(`server running @ http://localhost:${port}`) | ||
}) | ||
module.exports = app |
# 更新日志 | ||
### 3.0.1 | 2018.10.21 | ||
- 合并 PR([#351](https://github.com/Binaryify/NeteaseCloudMusicApi/pull/351)) | ||
- 文档增加 `/top/song` 接口 | ||
- `/banner` 换成 linux api,返回结构有所变动 | ||
- `/check/music` 已知 bug 修复 | ||
### 3.0.0 | 2018.10.14 | ||
## 整体 | ||
- 完善文档,增加之前没写进文档的接口说明 | ||
@@ -11,17 +23,18 @@ | ||
- 模块化, 剥离res,req, 方便导出调用 | ||
- 模块化, 剥离 res,req, 方便导出调用 | ||
- 增加 cookie-parser | ||
- 增加 cookie-parser | ||
### 参数修改 | ||
- `/song/detail` 增加多id支持 | ||
- `/toplist/detail` 移除参数 | ||
- `/song/detail` 增加多 id 支持 | ||
- `/resource/like` 增加参数 `type` | ||
- `/toplist/detail` 移除参数 | ||
- `/resource/like` 增加参数 `type` | ||
- `/top/playlist/highquality` 增加分页参数 `before` | ||
### 统一参数 | ||
- `/artist/sub` 与 `artist/unsub` 合并, 用`query.t` | ||
@@ -33,5 +46,6 @@ | ||
### URL重命名 | ||
- `/video` 改为 `video/url` | ||
### URL 重命名 | ||
- `/video` 改为 `video/url` | ||
- `/mv` 改为 `mv/detail` | ||
@@ -42,2 +56,3 @@ | ||
### 转发逻辑修改 | ||
- `/toplist/artist` 换成 weapi | ||
@@ -47,10 +62,12 @@ | ||
### BUG修复 | ||
### BUG 修复 | ||
- `/playlist/create`, `/playlist/update` 被判欺骗,增加 cookie | ||
### 路由增删 | ||
- 删除 `/recommend/dislike` | ||
- 增加 `/video/sub` (收藏视频), `/mv/sub` (收藏MV) | ||
- 删除 `/recommend/dislike` | ||
- 增加 `/video/sub` (收藏视频), `/mv/sub` (收藏 MV) | ||
- 增加 `/video/detail` (视频详情) | ||
@@ -61,4 +78,5 @@ | ||
### 2.20.5 | 2018.09.29 | ||
修复非法参数403 #335, 修复代理错误 #334 | ||
修复非法参数 403 #335, 修复代理错误 #334 | ||
### 2.20.4 | 2018.09.27 | ||
@@ -65,0 +83,0 @@ |
@@ -1,2 +0,2 @@ | ||
// 我的歌手列表 | ||
// 关注歌手列表 | ||
@@ -3,0 +3,0 @@ module.exports = (query, request) => { |
@@ -5,17 +5,5 @@ // 首页轮播图 | ||
return request( | ||
'GET', `http://music.163.com/discover`, {}, | ||
{ua: 'pc', proxy: query.proxy} | ||
'POST', `http://music.163.com/api/v2/banner/get`, {clientType: "pc"}, | ||
{crypto: 'linuxapi', proxy: query.proxy} | ||
) | ||
.then(response => { | ||
try{ | ||
const banners = eval(`(${/Gbanners\s*=\s*([^;]+);/.exec(response.body)[1]})`) | ||
response.body = {code: 200, banners: banners} | ||
return response | ||
} | ||
catch(err){ | ||
response.status = 500 | ||
response.body = {code: 500, msg: err.stack} | ||
return Promise.reject(response) | ||
} | ||
}) | ||
} |
@@ -13,8 +13,12 @@ // 歌曲可用性 | ||
.then(response => { | ||
if (response.body.code == 200) { | ||
if (response.body.data[0].code == 200){ | ||
response.body = {success: true, message: 'ok'} | ||
return response | ||
let playable = false | ||
if(response.body.code == 200){ | ||
if(response.body.data[0].code == 200){ | ||
playable = true | ||
} | ||
} | ||
if(playable){ | ||
response.body = {success: true, message: 'ok'} | ||
return response | ||
} | ||
else{ | ||
@@ -21,0 +25,0 @@ response.status = 404 |
@@ -1,2 +0,2 @@ | ||
// 我的电台列表 | ||
// 订阅电台列表 | ||
@@ -3,0 +3,0 @@ module.exports = (query, request) => { |
@@ -1,2 +0,2 @@ | ||
// 红心取消红心歌曲 | ||
// 红心与取消红心歌曲 | ||
@@ -3,0 +3,0 @@ module.exports = (query, request) => { |
@@ -0,1 +1,3 @@ | ||
// MV链接 | ||
module.exports = (query, request) => { | ||
@@ -2,0 +4,0 @@ const data = { |
// 私信歌单 | ||
module.exports = (query, request) => { | ||
query.cookie.os = 'pc' | ||
const data = { | ||
@@ -5,0 +6,0 @@ id: query.playlist, |
// 私信 | ||
module.exports = (query, request) => { | ||
query.cookie.os = 'pc' | ||
const data = { | ||
@@ -5,0 +6,0 @@ id: query.playlist, |
@@ -1,8 +0,16 @@ | ||
// 最新单曲(暂时废弃?) | ||
// 新歌速递 | ||
module.exports = (query, request) => { | ||
return request( | ||
'POST', `http://music.163.com/weapi/v1/discovery/new/songs`, {}, | ||
{crypto: 'weapi', cookie: query.cookie, proxy: query.proxy} | ||
) | ||
} | ||
const data = { | ||
areaId: query.type || 0, // 全部:0 华语:7 欧美:96 日本:8 韩国:16 | ||
limit: query.limit || 100, | ||
offset: query.offset || 0, | ||
total: true | ||
} | ||
return request( | ||
'POST', | ||
`http://music.163.com/weapi/v1/discovery/new/songs`, | ||
data, | ||
{ crypto: 'weapi', cookie: query.cookie, proxy: query.proxy } | ||
) | ||
} |
@@ -1,2 +0,2 @@ | ||
// 云盘数据详情?(暂时不要使用) | ||
// 云盘数据详情(暂时不要使用) | ||
@@ -3,0 +3,0 @@ module.exports = (query, request) => { |
{ | ||
"name": "NeteaseCloudMusicApi", | ||
"version": "3.0.0", | ||
"version": "3.0.1", | ||
"description": "网易云音乐 NodeJS 版 API", | ||
@@ -19,3 +19,2 @@ "scripts": { | ||
"apicache": "^1.2.1", | ||
"big-integer": "^1.6.28", | ||
"express": "^4.16.3", | ||
@@ -29,2 +28,2 @@ "request": "^2.85.0" | ||
} | ||
} | ||
} |
@@ -1,67 +0,34 @@ | ||
// 参考 https://github.com/darknessomi/musicbox/wiki/ | ||
'use strict' | ||
const crypto = require('crypto') | ||
const bigInt = require('big-integer') | ||
const modulus = | ||
'00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7' | ||
const nonce = '0CoJUm6Qyw8W8jud' | ||
const pubKey = '010001' | ||
const iv = Buffer.from('0102030405060708') | ||
const presetKey = Buffer.from('0CoJUm6Qyw8W8jud') | ||
const linuxapiKey = Buffer.from('rFgB&h#%2?^eDg:Q') | ||
const base62 = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' | ||
const publicKey = '-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDgtQn2JZ34ZC28NWYpAUd98iZ37BUrX/aKzmFbt7clFSs6sXqHauqKWqdtLkF2KexO40H1YTX8z2lSgBBOAxLsvaklV8k4cBFK9snQXE9/DDaFt6Rr7iVZMldczhC0JNgTz+SHXT6CBHuX3e9SdB1Ua44oncaTWz7OBGLbCiK45wIDAQAB\n-----END PUBLIC KEY-----' | ||
String.prototype.hexEncode = function() { | ||
let hex, i | ||
let result = '' | ||
for (i = 0; i < this.length; i++) { | ||
hex = this.charCodeAt(i).toString(16) | ||
result += ('' + hex).slice(-4) | ||
} | ||
return result | ||
const aesEncrypt = (buffer, mode, key, iv) => { | ||
const cipher = crypto.createCipheriv('aes-128-' + mode, key, iv) | ||
return Buffer.concat([cipher.update(buffer),cipher.final()]) | ||
} | ||
function createSecretKey(size) { | ||
const keys = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' | ||
let key = '' | ||
for (let i = 0; i < size; i++) { | ||
let pos = Math.random() * keys.length | ||
pos = Math.floor(pos) | ||
key = key + keys.charAt(pos) | ||
} | ||
return key | ||
const rsaEncrypt = (buffer, key) => { | ||
buffer = Buffer.concat([Buffer.alloc(128 - buffer.length), buffer]) | ||
return crypto.publicEncrypt({key: key, padding: crypto.constants.RSA_NO_PADDING}, buffer) | ||
} | ||
function aesEncrypt(text, secKey) { | ||
const _text = text | ||
const lv = new Buffer('0102030405060708', 'binary') | ||
const _secKey = new Buffer(secKey, 'binary') | ||
const cipher = crypto.createCipheriv('AES-128-CBC', _secKey, lv) | ||
let encrypted = cipher.update(_text, 'utf8', 'base64') | ||
encrypted += cipher.final('base64') | ||
return encrypted | ||
const weapi = (object) => { | ||
const text = JSON.stringify(object) | ||
const secretKey = crypto.randomBytes(16).map(n => (base62.charAt(n % 62).charCodeAt())) | ||
return { | ||
params: aesEncrypt(Buffer.from(aesEncrypt(Buffer.from(text), 'cbc', presetKey, iv).toString('base64')), 'cbc', secretKey, iv).toString('base64'), | ||
encSecKey: rsaEncrypt(secretKey.reverse(), publicKey).toString('hex') | ||
} | ||
} | ||
function zfill(str, size) { | ||
while (str.length < size) str = '0' + str | ||
return str | ||
const linuxapi = (object) => { | ||
const text = JSON.stringify(object) | ||
return { | ||
eparams: aesEncrypt(Buffer.from(text), 'ecb', linuxapiKey, '').toString('hex').toUpperCase() | ||
} | ||
} | ||
function rsaEncrypt(text, pubKey, modulus) { | ||
const _text = text.split('').reverse().join('') | ||
const biText = bigInt(new Buffer(_text).toString('hex'), 16), | ||
biEx = bigInt(pubKey, 16), | ||
biMod = bigInt(modulus, 16), | ||
biRet = biText.modPow(biEx, biMod) | ||
return zfill(biRet.toString(16), 256) | ||
} | ||
function Encrypt(obj) { | ||
const text = JSON.stringify(obj) | ||
const secKey = createSecretKey(16) | ||
const encText = aesEncrypt(aesEncrypt(text, nonce), secKey) | ||
const encSecKey = rsaEncrypt(secKey, pubKey, modulus) | ||
return { | ||
params: encText, | ||
encSecKey: encSecKey | ||
} | ||
} | ||
module.exports = Encrypt | ||
module.exports = {weapi, linuxapi} |
@@ -1,8 +0,8 @@ | ||
const encrypt = require('./crypto.js') | ||
const encrypt = require('./crypto') | ||
const request = require('request') | ||
const queryString = require('querystring') | ||
// request.debug = false | ||
request.debug = true | ||
function chooseUserAgent(ua) { | ||
const chooseUserAgent = (ua) => { | ||
const userAgentList = [ | ||
@@ -36,3 +36,3 @@ 'Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1', | ||
function createRequest(method, url, data, options){ | ||
const createRequest = (method, url, data, options) => { | ||
return new Promise((resolve, reject) => { | ||
@@ -42,3 +42,3 @@ | ||
if(method.toUpperCase() == 'POST') headers['Content-Type'] = 'application/x-www-form-urlencoded' | ||
if(url.indexOf('music.163.com') != -1) headers['Referer'] = 'http://music.163.com' | ||
if(url.includes('music.163.com')) headers['Referer'] = 'http://music.163.com' | ||
// headers['X-Real-IP'] = '118.88.88.88' | ||
@@ -52,6 +52,12 @@ | ||
if(options.crypto == 'weapi'){ | ||
const csrfToken = (headers['Cookie'] || '').match(/_csrf=([^(;|$)]+)/) | ||
let csrfToken = (headers['Cookie'] || '').match(/_csrf=([^(;|$)]+)/) | ||
data.csrf_token = (csrfToken ? csrfToken[1] : '') | ||
data = encrypt(data) | ||
data = encrypt.weapi(data) | ||
url = url.replace(/\w*api/,'weapi') | ||
} | ||
else if(options.crypto == 'linuxapi'){ | ||
data = encrypt.linuxapi({'method': method, url: url.replace(/\w*api/,'api'), 'params': data}) | ||
headers['User-Agent'] = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36' | ||
url = 'http://music.163.com/api/linux/forward' | ||
} | ||
@@ -72,5 +78,2 @@ const answer = {status: 500, body: {}, cookie: []} | ||
answer.status = answer.body.code || res.statusCode | ||
if(answer.body.code=='301'){ | ||
answer.body.apiMsg='需要登陆' | ||
} | ||
} | ||
@@ -77,0 +80,0 @@ catch(e){ |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
3
1
139637
123
1492
+ Addedobject-inspect@1.13.3(transitive)
- Removedbig-integer@^1.6.28
- Removedbig-integer@1.6.52(transitive)
- Removedobject-inspect@1.13.4(transitive)