diff --git a/README.MD b/README.MD index a0768c0..6135715 100644 --- a/README.MD +++ b/README.MD @@ -426,6 +426,9 @@ banner({ type: 0 }).then((res) => { 280. 获取专辑歌曲的音质 281. 歌手动态信息 282. 最近听歌列表 +283. 云盘导入歌曲 +284. 获取客户端歌曲下载链接 - 新版 +285. 当前账号关注的用户/歌手 ## 单元测试 diff --git a/module/aidj_content_rcmd.js b/module/aidj_content_rcmd.js index e580b1d..f8e118c 100644 --- a/module/aidj_content_rcmd.js +++ b/module/aidj_content_rcmd.js @@ -2,7 +2,7 @@ // 实际请求参数如下, 部分内容省略, 敏感信息已进行混淆 // 可按需修改此 API 的代码 -/* {"extInfo":"{\"lastRequestTimestamp\":1692358373509,\"lbsInfoList\":[{\"lat\":40.23076381,\"lon\":129.07545186,\"time\":1692358543},{\"lat\":40.23076381,\"lon\":129.07545186,\"time\":1692055283}],\"listenedTs\":false,\"noAidjToAidj\":true}","header":"{}","e_r":true} */ +/* {"extInfo":"{\"lastRequestTimestamp\":1692358373509,\"lbsInfoList\":[{\"lat\":40.23076381,\"lon\":129.07545186,\"time\":1692358543},{\"lat\":40.23076381,\"lon\":129.07545186,\"time\":1692055283}],\"listenedTs\":false,\"noAidjToAidj\":true}","header":"{}"} */ const createOption = require('../util/option.js') module.exports = (query, request) => { diff --git a/module/api.js b/module/api.js index f4d5eac..6de8689 100644 --- a/module/api.js +++ b/module/api.js @@ -2,8 +2,14 @@ const createOption = require('../util/option.js') module.exports = (query, request) => { const method = query.method || 'POST' const uri = query.uri - const data = - typeof query.data === 'string' ? JSON.parse(query.data) : query.data || {} + let data = {} + try { + data = + typeof query.data === 'string' ? JSON.parse(query.data) : query.data || {} + } catch (e) { + data = {} + } + const crypto = query.crypto || '' const res = request(method, uri, data, createOption(query, crypto)) diff --git a/module/song_download_url_v1.js b/module/song_download_url_v1.js new file mode 100644 index 0000000..00bbaaf --- /dev/null +++ b/module/song_download_url_v1.js @@ -0,0 +1,18 @@ +// 获取客户端歌曲下载链接 - v1 +// 此版本不再采用 br 作为音质区分的标准 +// 而是采用 standard, exhigh, lossless, hires, jyeffect(高清环绕声), sky(沉浸环绕声), jymaster(超清母带) 进行音质判断 + +const createOption = require('../util/option.js') +module.exports = (query, request) => { + const data = { + id: query.id, + immerseType: 'c51', + level: query.level, + } + return request( + 'POST', + `/api/song/enhance/download/url/v1`, + data, + createOption(query), + ) +} diff --git a/module/user_follow_mixed.js b/module/user_follow_mixed.js new file mode 100644 index 0000000..eac883e --- /dev/null +++ b/module/user_follow_mixed.js @@ -0,0 +1,24 @@ +// 当前账号关注的用户/歌手 + +const createOption = require('../util/option.js') +module.exports = (query, request) => { + const size = query.size || 30 + const cursor = query.cursor || 0 + const scene = query.scene || 0 // 0: 所有关注 1: 关注的歌手 2: 关注的用户 + const data = { + authority: 'false', + page: JSON.stringify({ + size: size, + cursor: cursor, + }), + scene: scene, + size: size, + sortType: '0', + } + return request( + 'POST', + `/api/user/follow/users/mixed/get/v2`, + data, + createOption(query), + ) +} diff --git a/public/docs/home.md b/public/docs/home.md index 6677ace..ad2b56e 100644 --- a/public/docs/home.md +++ b/public/docs/home.md @@ -299,6 +299,8 @@ 281. 歌手动态信息 282. 最近听歌列表 283. 云盘导入歌曲 +284. 获取客户端歌曲下载链接 - 新版 +285. 当前账号关注的用户/歌手 ## 安装 @@ -4685,6 +4687,33 @@ bitrate = Math.floor(br / 1000) ``` 导入后的文件名后缀均为 `.mp3` 。但用 `获取音乐url` 获取到的文件格式仍然是正确的。 +### 获取客户端歌曲下载链接 - 新版 + +说明 : 使用 `/song/url/v1` 接口获取的是歌曲试听 url, 但存在部分歌曲在非 VIP 账号上可以下载无损音质而不能试听无损音质, 使用此接口可使非 VIP 账号获取这些歌曲的无损音频 + +**必选参数 :** `id` : 音乐 id + `level`: 播放音质等级, 分为 `standard` => `标准`,`higher` => `较高`, `exhigh`=>`极高`, +`lossless`=>`无损`, `hires`=>`Hi-Res`, `jyeffect` => `高清环绕声`, `sky` => `沉浸环绕声`, +`jymaster` => `超清母带` + +**接口地址 :** `/song/download/url/v1` + +**调用例子 :** `/song/download/url/v1?id=2058263032&level=lossless` + +### 当前账号关注的用户/歌手 + +说明 : 调用此接口, 可获得当前账号关注的用户/歌手 + +**可选参数 :** `size` : 返回数量 , 默认为 30 + +`cursor` : 返回数据的 cursor, 默认为 0 , 传入上一次返回结果的 cursor,将会返回下一页的数据 + +`scene` : 场景, 0 表示所有关注, 1 表示关注的歌手, 2 表示关注的用户, 默认为 0 + +**接口地址 :** `/user/follow/mixed` + +**调用例子 :** `/user/follow/mixed?scene=1` + ## 离线访问此文档 此文档同时也是 Progressive Web Apps(PWA), 加入了 serviceWorker, 可离线访问 diff --git a/util/config.json b/util/config.json index 33de888..3dbfad1 100644 --- a/util/config.json +++ b/util/config.json @@ -13,6 +13,6 @@ "apiDomain": "https://interface.music.163.com", "domain": "https://music.163.com", "encrypt": true, - "encryptResponse": true + "encryptResponse": false } } \ No newline at end of file diff --git a/util/option.js b/util/option.js index 3768ead..ae0bd2e 100644 --- a/util/option.js +++ b/util/option.js @@ -5,6 +5,7 @@ const createOption = (query, crypto = '') => { ua: query.ua || '', proxy: query.proxy, realIP: query.realIP, + e_r: query.e_r || undefined, } } module.exports = createOption diff --git a/util/request.js b/util/request.js index 9f51510..9bb6d06 100644 --- a/util/request.js +++ b/util/request.js @@ -85,74 +85,95 @@ const createRequest = (method, uri, data = {}, options) => { } // console.log(options.cookie, headers['Cookie']) - let url = '' - // 目前任意uri都支持三种加密方式 - if (options.crypto === 'weapi') { - headers['Referer'] = 'https://music.163.com' - headers['User-Agent'] = options.ua || chooseUserAgent('pc') - let csrfToken = (headers['Cookie'] || '').match(/_csrf=([^(;|$)]+)/) - data.csrf_token = csrfToken ? csrfToken[1] : '' - data = encrypt.weapi(data) - url = APP_CONF.domain + '/weapi/' + uri.substr(5) - } else if (options.crypto === 'linuxapi') { - headers['User-Agent'] = - 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36' - data = encrypt.linuxapi({ - method: method, - url: APP_CONF.apiDomain + uri, - params: data, - }) - url = 'https://music.163.com/api/linux/forward' - } else if ( - options.crypto === 'eapi' || - options.crypto === 'api' || - options.crypto === '' - ) { - // 两种加密方式,都应生成客户端的cookie - const cookie = options.cookie || {} - const csrfToken = cookie['__csrf'] || '' - const header = { - osver: cookie.osver || '17.4.1', //系统版本 - deviceId: cookie.deviceId || global.deviceId, - appver: cookie.appver || iosAppVersion, // app版本 - versioncode: cookie.versioncode || '140', //版本号 - mobilename: cookie.mobilename || '', //设备model - buildver: cookie.buildver || Date.now().toString().substr(0, 10), - resolution: cookie.resolution || '1920x1080', //设备分辨率 - __csrf: csrfToken, - os: cookie.os || 'ios', - channel: cookie.channel || '', - requestId: `${Date.now()}_${Math.floor(Math.random() * 1000) - .toString() - .padStart(4, '0')}`, - } - if (cookie.MUSIC_U) header['MUSIC_U'] = cookie.MUSIC_U - if (cookie.MUSIC_A) header['MUSIC_A'] = cookie.MUSIC_A - headers['Cookie'] = Object.keys(header) - .map( - (key) => - encodeURIComponent(key) + '=' + encodeURIComponent(header[key]), - ) - .join('; ') + let url = '', + encryptData = '', + crypto = options.crypto, + csrfToken = cookie['__csrf'] || '' + // 根据加密方式加密请求数据;目前任意uri都支持四种加密方式 + switch (crypto) { + case 'weapi': + headers['Referer'] = 'https://music.163.com' + headers['User-Agent'] = options.ua || chooseUserAgent('pc') + data.csrf_token = csrfToken + encryptData = encrypt.weapi(data) + url = APP_CONF.domain + '/weapi/' + uri.substr(5) + break - let eapiEncrypt = () => { - data.header = header - data = encrypt.eapi(uri, data) - url = APP_CONF.apiDomain + '/eapi/' + uri.substr(5) - } - if (options.crypto === 'eapi') { - eapiEncrypt() - } else if (options.crypto === 'api') { - url = APP_CONF.apiDomain + uri - } else if (options.crypto === '') { - // 加密方式为空,以配置文件的加密方式为准 - if (APP_CONF.encrypt) { - eapiEncrypt() - } else url = APP_CONF.apiDomain + uri - } - } else { - // 未知的加密方式 - console.log('[ERR]', 'Unknown Crypto:', options.crypto) + case 'linuxapi': + headers['User-Agent'] = + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36' + encryptData = encrypt.linuxapi({ + method: method, + url: APP_CONF.apiDomain + uri, + params: data, + }) + url = 'https://music.163.com/api/linux/forward' + break + + case 'eapi': + case 'api': + case '': + // 两种加密方式,都应生成客户端的cookie + const cookie = options.cookie || {} + const header = { + osver: cookie.osver || '17.4.1', //系统版本 + deviceId: cookie.deviceId || global.deviceId, + appver: cookie.appver || iosAppVersion, // app版本 + versioncode: cookie.versioncode || '140', //版本号 + mobilename: cookie.mobilename || '', //设备model + buildver: cookie.buildver || Date.now().toString().substr(0, 10), + resolution: cookie.resolution || '1920x1080', //设备分辨率 + __csrf: csrfToken, + os: cookie.os || 'ios', + channel: cookie.channel || '', + requestId: `${Date.now()}_${Math.floor(Math.random() * 1000) + .toString() + .padStart(4, '0')}`, + } + if (cookie.MUSIC_U) header['MUSIC_U'] = cookie.MUSIC_U + if (cookie.MUSIC_A) header['MUSIC_A'] = cookie.MUSIC_A + headers['Cookie'] = Object.keys(header) + .map( + (key) => + encodeURIComponent(key) + '=' + encodeURIComponent(header[key]), + ) + .join('; ') + + let eapi = () => { + // 使用eapi加密 + data.header = header + data.e_r = + options.e_r != undefined + ? options.e_r + : data.e_r != undefined + ? data.e_r + : APP_CONF.encryptResponse // 用于加密接口返回值 + encryptData = encrypt.eapi(uri, data) + url = APP_CONF.apiDomain + '/eapi/' + uri.substr(5) + } + let api = () => { + // 不使用任何加密 + url = APP_CONF.apiDomain + uri + encryptData = data + } + if (crypto === 'eapi') { + eapi() + } else if (crypto === 'api') { + api() + } else if (crypto === '') { + // 加密方式为空,以配置文件的加密方式为准 + if (APP_CONF.encrypt) { + eapi() + } else { + api() + } + } + break + + default: + // 未知的加密方式 + console.log('[ERR]', 'Unknown Crypto:', crypto) + break } const answer = { status: 500, body: {}, cookie: [] } // console.log(headers, 'headers') @@ -160,11 +181,19 @@ const createRequest = (method, uri, data = {}, options) => { method: method, url: url, headers: headers, - data: new URLSearchParams(data).toString(), + data: new URLSearchParams(encryptData).toString(), httpAgent: new http.Agent({ keepAlive: true }), httpsAgent: new https.Agent({ keepAlive: true }), } + if (data.e_r) { + settings = { + ...settings, + encoding: null, + responseType: 'arraybuffer', + } + } + if (options.proxy) { if (options.proxy.indexOf('pac') > -1) { settings.httpAgent = new PacProxyAgent(options.proxy) @@ -201,7 +230,16 @@ const createRequest = (method, uri, data = {}, options) => { x.replace(/\s*Domain=[^(;|$)]+;*/, ''), ) try { - answer.body = JSON.parse(body.toString()) + if (data.e_r) { + // eapi接口返回值被加密,需要解密 + answer.body = encrypt.eapiResDecrypt( + body.toString('hex').toUpperCase(), + ) + } else { + answer.body = + typeof body == 'object' ? body : JSON.parse(body.toString()) + } + if (answer.body.code) { answer.body.code = Number(answer.body.code) } @@ -216,13 +254,8 @@ const createRequest = (method, uri, data = {}, options) => { } } catch (e) { // console.log(e) - try { - answer.body = JSON.parse(encrypt.decrypt(body)) - } catch (err) { - // console.log(err) - // can't decrypt and can't parse directly - answer.body = body - } + // can't decrypt and can't parse directly + answer.body = body answer.status = res.status }