diff --git a/.npmignore b/.npmignore index 3f24434..1d6a290 100644 --- a/.npmignore +++ b/.npmignore @@ -1,3 +1,4 @@ static docs -node_modules \ No newline at end of file +node_modules +module_example diff --git a/CHANGELOG.MD b/CHANGELOG.MD index 0be367e..5d77996 100644 --- a/CHANGELOG.MD +++ b/CHANGELOG.MD @@ -1,4 +1,11 @@ # 更新日志 +### 4.0.0 | 2021.1.03 +- 新增云盘上传接口,新增二维码登录相关接口和相关demo(http://localhost:3000/qrlogin.html, http://localhost:3000/cloud.html),更新 d.ts + +- 升级部分接口加密方法("linuxapi" 都替换到了"api") + +- 更新 `login/status` 接口(返回字段和之前不一样) + ### 3.47.5 | 2020.12.20 - 更新appver [#1060](https://github.com/Binaryify/NeteaseCloudMusicApi/issues/1060) diff --git a/docs/README.md b/docs/README.md index 51698bf..4553a13 100644 --- a/docs/README.md +++ b/docs/README.md @@ -213,6 +213,10 @@ 195. 关注歌手新歌 196. 关注歌手新MV 197. 歌手详情 +198. 云盘上传 +199. 二维码登录 +200. 话题详情 +201. 话题详情热门动态 ## 安装 @@ -359,7 +363,7 @@ $ sudo docker run -d -p 3000:3000 netease-music-api !> 部分接口如登录接口不能调用太频繁 , 否则可能会触发 503 错误或者 ip 高频错误 ,若需频繁调用 , 需要准备 IP 代理池 (更新:已加入缓存机制,但仍需注意). -!> 本项目仅供学习使用,请尊重版权,请勿利用此项目从事商业行为 +!> 本项目仅供学习使用,请尊重版权,请勿利用此项目从事商业行为或进行破坏版权行为 !> 文档可能会有缓存 , 如果文档版本和 github 上的版本不一致,请清除缓存再查看 @@ -420,22 +424,34 @@ v3.30.0后支持手动传入cookie,登录接口返回内容新增 `cookie` 字 } ``` #### 3. 二维码登录 -说明: 二维码登录涉及到3个接口 -1. 二维码key生成接口 -说明: 调用此接口可生成一个key +说明: 二维码登录涉及到3个接口,调用务必带上时间戳,防止缓存 +##### 1. 二维码key生成接口 + +说明: 调用此接口可生成一个key + **接口地址 :** `/login/qr/key` +##### 2. 二维码生成接口 +说明: 调用此接口传入上一个接口生成的key可生成二维码图片的base64和二维码信息,可使用base64展示图片,或者使用二维码信息内容自行使用第三方二维码生产库渲染二维码 -2. 二维码生成接口 -说明: 调用此接口传入上一个接口生成的key可生成二维码图片的base64和二维码信息,可使用base64展示图片,或者使用二维码信息内容自行使用第三方二维码生产库渲染二维码 -可选参数: `qrimg` 传入后 -**接口地址 :** `/login/qr/create` +必选参数: `key`,由第一个接口生成 + +可选参数: `qrimg` 传入后会额外返回二维码图片base64编码 + +**接口地址 :** `/login/qr/create` + +**调用例子 :** `/login/qr/create?key=xxx` -3. 二维码检测扫码状态接口 -说明: 轮询此接口可获取二维码扫码状态,801为等待扫码,802为待确认,803为授权登陆成功 +##### 3. 二维码检测扫码状态接口 +说明: 轮询此接口可获取二维码扫码状态,800为二维码过期,801为等待扫码,802为待确认,803为授权登录成功(803状态码下会返回cookies) + +必选参数: `key`,由第一个接口生成 **接口地址 :** `/login/qr/check` +**调用例子 :** `/login/qr/check?key=xxx` + +调用可参考项目文件例子`/public/qrlogin.html` (访问地址:http://localhost:3000/qrlogin.html) #### 注意 @@ -640,7 +656,7 @@ signature:用户签名 **调用例子 :** `/user/update?gender=0&signature=测试签名&city=440300&nickname=binary&birthday=1525918298004&province=440000` ### 更新头像 -说明 : 登录后调用此接口,使用`'Content-Type': 'multipart/form-data'`上传图片formData(name为'imgFile'),可更新头像(参考:https://github.com/Binaryify/NeteaseCloudMusicApi/blob/master/public/avatar_update.html) +说明 : 登录后调用此接口,使用`'Content-Type': 'multipart/form-data'`上传图片formData(name为'imgFile'),可更新头像(参考:https://github.com/Binaryify/NeteaseCloudMusicApi/blob/master/public/avatar_update.html),支持命令行调用,参考module_example目录下`avatar_upload.js` **可选参数 :** @@ -936,6 +952,21 @@ tags: 歌单标签 **调用例子 :** `/hot/topic?limit=30&offset=30` +### 获取话题详情 + +说明 : 调用此接口 , 可获取话题详情 + +**接口地址 :** `/topic/detail` + +**调用例子 :** `/topic/detail?actid=111551188` +### 获取话题详情热门动态 + +说明 : 调用此接口 , 可获取话题详情热门动态 + +**接口地址 :** `/topic/detail/event/hot` + +**调用例子 :** `/topic/detail/event/hot?actid=111551188` + ### 云村热评 说明 : 登录后调用此接口 , 可获取云村热评 @@ -1238,10 +1269,6 @@ mp3url 不能直接用 , 可通过 `/song/url` 接口传入歌曲 id 获取具 **调用例子 :** `/search?keywords= 海阔天空` `/cloudsearch?keywords= 海阔天空` -返回数据如下图 : -![搜索音乐](https://raw.githubusercontent.com/Binaryify/NeteaseCloudMusicApi/master/static/%E6%90%9C%E7%B4%A2.png) - - ### 默认搜索关键词 说明 : 调用此接口 , 可获取默认搜索关键词 @@ -1443,8 +1470,7 @@ mp3url 不能直接用 , 可通过 `/song/url` 接口传入歌曲 id 获取具 ### 歌曲评论 -说明 : 调用此接口 , 传入音乐 id 和 limit 参数 , 可获得该音乐的所有评论 ( 不需要 -登录 ) +说明 : 调用此接口 , 传入音乐 id 和 limit 参数 , 可获得该音乐的所有评论 ( 不需要登录 ) **必选参数 :** `id`: 音乐 id @@ -2553,6 +2579,19 @@ type : 地区 **调用例子 :** `/user/cloud/del` +### 云盘上传 +说明 : 登录后调用此接口,使用`'Content-Type': 'multipart/form-data'`上传mp3 formData(name为'songFile'),可上传歌曲到云盘 + +参考: https://github.com/Binaryify/NeteaseCloudMusicApi/blob/master/public/cloud.html + +访问地址: http://localhost:3000/qrlogin.html) + +支持命令行调用,参考module_example目录下`song_upload.js` + +**接口地址 :** `/cloud` + +**调用例子 :** `/cloud` + ### 电台banner 说明 : 调用此接口,可获取电台banner diff --git a/interface.d.ts b/interface.d.ts index 461179c..ee6e622 100644 --- a/interface.d.ts +++ b/interface.d.ts @@ -1360,3 +1360,32 @@ export function artist_detail( id: number | string } & RequestBaseConfig, ): Promise + +export function cloud(params: RequestBaseConfig): Promise + +export function topic_detail( + params: { + actid?: number | string + } & RequestBaseConfig, +): Promise + +export function topic_detail_event_hot( + params: { + actid?: number | string + } & RequestBaseConfig, +): Promise + +export function login_qr_key(params: RequestBaseConfig): Promise + +export function login_qr_create( + params: { + key?: number | string + qrimg?: boolean | string + } & RequestBaseConfig, +): Promise + +export function login_qr_check( + params: { + key?: number | string + } & RequestBaseConfig, +): Promise diff --git a/module/banner.js b/module/banner.js index e2c5b0d..00f52c9 100644 --- a/module/banner.js +++ b/module/banner.js @@ -12,6 +12,6 @@ module.exports = (query, request) => { 'POST', `https://music.163.com/api/v2/banner/get`, { clientType: type }, - { crypto: 'linuxapi', proxy: query.proxy, realIP: query.realIP }, + { crypto: 'api', proxy: query.proxy, realIP: query.realIP }, ) } diff --git a/module/cloud.js b/module/cloud.js new file mode 100644 index 0000000..ae350f0 --- /dev/null +++ b/module/cloud.js @@ -0,0 +1,132 @@ +const mm = require('music-metadata') +const uploadPlugin = require('../plugins/songUpload') +const md5 = require('md5') +module.exports = async (query, request) => { + query.cookie.os = 'pc' + query.cookie.appver = '2.7.1.198277' + const bitrate = 999000 + if (!query.songFile) { + return Promise.reject({ + status: 500, + body: { + msg: '请上传音乐文件', + code: 500, + }, + }) + } + if (!query.songFile.md5) { + // 命令行上传没有md5和size信息,需要填充 + query.songFile.md5 = md5(query.songFile.data) + query.songFile.size = query.songFile.data.byteLength + } + const res = await request( + 'POST', + `https://interface.music.163.com/api/cloud/upload/check`, + { + bitrate: String(bitrate), + ext: '', + length: query.songFile.size, + md5: query.songFile.md5, + songId: '0', + version: 1, + }, + { + crypto: 'weapi', + cookie: query.cookie, + proxy: query.proxy, + realIP: query.realIP, + }, + ) + let artist = '' + let album = '' + let songName = '' + try { + const metadata = await mm.parseBuffer(query.songFile.data, 'audio/mpeg') + if (metadata.native.ID3v1) { + metadata.native.ID3v1.forEach((item) => { + // console.log(item.id, item.value) + if (item.id === 'title') { + songName = item.value + } + if (item.id === 'artist') { + artist = item.value + } + if (item.id === 'album') { + album = item.value + } + }) + // console.log({ + // songName, + // album, + // songName, + // }) + } + } catch (error) { + console.log(error) + } + const tokenRes = await request( + 'POST', + `https://music.163.com/weapi/nos/token/alloc`, + { + bucket: '', + ext: 'mp3', + filename: query.songFile.name.replace('.mp3', ''), + local: false, + nos_product: 3, + type: 'audio', + md5: query.songFile.md5, + }, + { crypto: 'weapi', cookie: query.cookie, proxy: query.proxy }, + ) + + if (res.body.needUpload) { + const uploadInfo = await uploadPlugin(query, request) + // console.log('uploadInfo', uploadInfo.body.result.resourceId) + } + // console.log(tokenRes.body.result) + const res2 = await request( + 'POST', + `https://music.163.com/api/upload/cloud/info/v2`, + { + md5: query.songFile.md5, + songid: res.body.songId, + filename: query.songFile.name, + song: songName || query.songFile.name.replace('.mp3', ''), + album: album || '未知专辑', + artist: artist || '未知艺术家', + bitrate: String(bitrate), + resourceId: tokenRes.body.result.resourceId, + }, + { + crypto: 'weapi', + cookie: query.cookie, + proxy: query.proxy, + realIP: query.realIP, + }, + ) + // console.log({ res2, privateCloud: res2.body.privateCloud }) + // console.log(res.body.songId, 'songid') + const res3 = await request( + 'POST', + `https://interface.music.163.com/api/cloud/pub/v2`, + { + songid: res2.body.songId, + }, + { + crypto: 'weapi', + cookie: query.cookie, + proxy: query.proxy, + realIP: query.realIP, + }, + ) + // console.log({ res3 }) + return { + status: 200, + body: { + ...res.body, + ...res3.body, + // ...uploadInfo, + }, + cookie: res.cookie, + } +} diff --git a/module/hot_topic.js b/module/hot_topic.js index 56d3dca..f901492 100644 --- a/module/hot_topic.js +++ b/module/hot_topic.js @@ -5,7 +5,7 @@ module.exports = (query, request) => { limit: query.limit || 20, offset: query.offset || 0, } - return request('POST', `https://music.163.com/weapi/act/hot`, data, { + return request('POST', `https://music.163.com/api/act/hot`, data, { crypto: 'weapi', cookie: query.cookie, proxy: query.proxy, diff --git a/module/login_qr_key.js b/module/login_qr_key.js index 23c2ef6..bbee4be 100644 --- a/module/login_qr_key.js +++ b/module/login_qr_key.js @@ -16,10 +16,8 @@ module.exports = async (query, request) => { return { status: 200, body: { - data: { - ...result.body, - code: 200, - }, + data: result.body, + code: 200, }, cookie: result.cookie, } diff --git a/module/lyric.js b/module/lyric.js index 1c1a7fb..c3e8721 100644 --- a/module/lyric.js +++ b/module/lyric.js @@ -9,7 +9,7 @@ module.exports = (query, request) => { tv: -1, } return request('POST', `https://music.163.com/api/song/lyric`, data, { - crypto: 'linuxapi', + crypto: 'api', cookie: query.cookie, proxy: query.proxy, realIP: query.realIP, diff --git a/module/playlist_detail.js b/module/playlist_detail.js index ad139e0..39718f7 100644 --- a/module/playlist_detail.js +++ b/module/playlist_detail.js @@ -7,7 +7,7 @@ module.exports = (query, request) => { s: query.s || 8, } return request('POST', `https://music.163.com/api/v6/playlist/detail`, data, { - crypto: 'linuxapi', + crypto: 'api', cookie: query.cookie, proxy: query.proxy, realIP: query.realIP, diff --git a/module/topic_detail.js b/module/topic_detail.js new file mode 100644 index 0000000..e0dd006 --- /dev/null +++ b/module/topic_detail.js @@ -0,0 +1,11 @@ +module.exports = (query, request) => { + const data = { + actid: query.actid, + } + return request('POST', `https://music.163.com/api/act/detail`, data, { + crypto: 'weapi', + cookie: query.cookie, + proxy: query.proxy, + realIP: query.realIP, + }) +} diff --git a/module/topic_detail_event_hot.js b/module/topic_detail_event_hot.js new file mode 100644 index 0000000..a979b6f --- /dev/null +++ b/module/topic_detail_event_hot.js @@ -0,0 +1,11 @@ +module.exports = (query, request) => { + const data = { + actid: query.actid, + } + return request('POST', `https://music.163.com/api/act/event/hot`, data, { + crypto: 'weapi', + cookie: query.cookie, + proxy: query.proxy, + realIP: query.realIP, + }) +} diff --git a/module/toplist.js b/module/toplist.js index 592b636..c690241 100644 --- a/module/toplist.js +++ b/module/toplist.js @@ -6,7 +6,7 @@ module.exports = (query, request) => { `https://music.163.com/api/toplist`, {}, { - crypto: 'linuxapi', + crypto: 'api', cookie: query.cookie, proxy: query.proxy, realIP: query.realIP, diff --git a/module/user_cloud.js b/module/user_cloud.js index fc4a8ab..3134960 100644 --- a/module/user_cloud.js +++ b/module/user_cloud.js @@ -5,7 +5,7 @@ module.exports = (query, request) => { limit: query.limit || 30, offset: query.offset || 0, } - return request('POST', `https://music.163.com/weapi/v1/cloud/get`, data, { + return request('POST', `https://music.163.com/api/v1/cloud/get`, data, { crypto: 'weapi', cookie: query.cookie, proxy: query.proxy, diff --git a/module_example/song_upload.js b/module_example/song_upload.js new file mode 100644 index 0000000..6973301 --- /dev/null +++ b/module_example/song_upload.js @@ -0,0 +1,23 @@ +const { cloud, login_cellphone } = require('../main') +const fs = require('fs') +const path = require('path') + +async function main() { + const result = await login_cellphone({ + phone: '手机号', + password: '密码', + }) + const filePath = './test.mp3' + try { + await cloud({ + songFile: { + name: path.basename(filePath), + data: fs.readFileSync(filePath), + }, + cookie: result.body.cookie, + }) + } catch (error) { + console.log(error, 'error') + } +} +main() diff --git a/module_example/test.mp3 b/module_example/test.mp3 new file mode 100644 index 0000000..e85b76e Binary files /dev/null and b/module_example/test.mp3 differ diff --git a/package.json b/package.json index fc960d8..963cd17 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "NeteaseCloudMusicApi", - "version": "3.47.5", + "version": "4.0.0", "description": "网易云音乐 NodeJS 版 API", "scripts": { "start": "node app.js", @@ -39,6 +39,8 @@ "axios": "^0.20.0", "express": "^4.17.1", "express-fileupload": "^1.1.9", + "md5": "^2.3.0", + "music-metadata": "^7.5.3", "pac-proxy-agent": "^4.0.0", "qrcode": "^1.4.4", "tunnel": "^0.0.6" diff --git a/plugins/songUpload.js b/plugins/songUpload.js new file mode 100644 index 0000000..c5c85f3 --- /dev/null +++ b/plugins/songUpload.js @@ -0,0 +1,39 @@ +const axios = require('axios') +module.exports = async (query, request) => { + // 获取key和token + const tokenRes = await request( + 'POST', + `https://music.163.com/weapi/nos/token/alloc`, + { + bucket: '', + ext: 'mp3', + filename: query.songFile.name.replace('.mp3', ''), + local: false, + nos_product: 3, + type: 'audio', + md5: query.songFile.md5, + }, + { crypto: 'weapi', cookie: query.cookie, proxy: query.proxy }, + ) + + // 上传 + const objectKey = tokenRes.body.result.objectKey.replace('/', '%2F') + try { + await axios({ + method: 'post', + url: `http://45.127.129.8/ymusic/${objectKey}?offset=0&complete=true&version=1.0`, + headers: { + 'x-nos-token': tokenRes.body.result.token, + 'Content-MD5': query.songFile.md5, + 'Content-Type': 'audio/mpeg', + 'Content-Length': String(query.songFile.size), + }, + data: query.songFile.data, + }) + } catch (error) { + console.log('error', error.response) + } + return { + ...tokenRes, + } +} diff --git a/public/avatar_update.html b/public/avatar_update.html index 45c2b82..7b0817f 100644 --- a/public/avatar_update.html +++ b/public/avatar_update.html @@ -1,82 +1,82 @@ + + + + 更新头像 + - - - - 更新头像 - - - - - - - - - - \ No newline at end of file + }) + } + + + diff --git a/public/cloud.html b/public/cloud.html new file mode 100644 index 0000000..0360e9c --- /dev/null +++ b/public/cloud.html @@ -0,0 +1,55 @@ + + + + + + 云盘上传 + + + + + + + + + diff --git a/public/playlist_cover_update.html b/public/playlist_cover_update.html index e6b1bd7..75e0fe6 100644 --- a/public/playlist_cover_update.html +++ b/public/playlist_cover_update.html @@ -1,88 +1,89 @@ + + + + 歌单封面上传 + - - - - 歌单封面上传 - + + + + + - - - - \ No newline at end of file + }) + } + + + diff --git a/public/test.html b/public/test.html index d0e8768..fccca52 100644 --- a/public/test.html +++ b/public/test.html @@ -23,36 +23,36 @@ $.ajax({ url: `/login/cellphone?phone=${phone}&password=${password}`, xhrFields: { - withCredentials: true //关键 + withCredentials: true, //关键 }, success: function (data) { console.log(data) $.ajax({ url: `/recommend/resource `, xhrFields: { - withCredentials: true //关键 + withCredentials: true, //关键 }, success: function (data) { console.log(data) }, error: function (err) { console.log(err) - } + }, }) }, error: function (err) { console.log(err) - } + }, }) axios({ url: `/login/cellphone?phone=${phone}&password=${password}`, - withCredentials: true //关键 + withCredentials: true, //关键 }).then(function (res) { console.log(res.data) axios({ url: `/recommend/resource`, - withCredentials: true //关键 + withCredentials: true, //关键 }).then(function (res) { console.log(res.data) }) @@ -60,4 +60,4 @@ - \ No newline at end of file +