chore(version): 更新api到4.28.0

Co-Authored-By: binaryify <binaryify@gmail.com>
This commit is contained in:
ImFurina 2025-09-07 13:20:37 +08:00
parent 5105a9a09d
commit 5478041ddd
24 changed files with 707 additions and 279 deletions

54
main.js
View File

@ -3,47 +3,61 @@ const path = require('path')
const tmpPath = require('os').tmpdir() const tmpPath = require('os').tmpdir()
const { cookieToJson } = require('./util') const { cookieToJson } = require('./util')
if (!fs.existsSync(path.resolve(tmpPath, 'anonymous_token'))) { const anonymousTokenPath = path.resolve(tmpPath, 'anonymous_token')
fs.writeFileSync(path.resolve(tmpPath, 'anonymous_token'), '', 'utf-8') if (!fs.existsSync(anonymousTokenPath)) {
fs.writeFileSync(anonymousTokenPath, '', 'utf-8')
} }
let firstRun = true
/** @type {Record<string, any>} */ /** @type {Record<string, any>} */
let obj = {} let obj = {}
fs.readdirSync(path.join(__dirname, 'module'))
.reverse() const modulePath = path.join(__dirname, 'module')
.forEach((file) => { const moduleFiles = fs.readdirSync(modulePath).reverse()
let requestModule = null
moduleFiles.forEach((file) => {
if (!file.endsWith('.js')) return if (!file.endsWith('.js')) return
let fileModule = require(path.join(__dirname, 'module', file))
const filePath = path.join(modulePath, file)
let fileModule = require(filePath)
let fn = file.split('.').shift() || '' let fn = file.split('.').shift() || ''
obj[fn] = function (data = {}) { obj[fn] = function (data = {}) {
if (typeof data.cookie === 'string') { const cookie =
data.cookie = cookieToJson(data.cookie) typeof data.cookie === 'string'
} ? cookieToJson(data.cookie)
: data.cookie || {}
return fileModule( return fileModule(
{ {
...data, ...data,
cookie: data.cookie ? data.cookie : {}, cookie,
}, },
async (...args) => { async (...args) => {
if (firstRun) { if (!requestModule) {
firstRun = false requestModule = require('./util/request')
const generateConfig = require('./generateConfig')
await generateConfig()
} }
// 待优化
const request = require('./util/request')
return request(...args) return requestModule(...args)
}, },
) )
} }
}) })
let serverModule = null
/** /**
* @type {Record<string, any> & import("./server")} * @type {Record<string, any> & import("./server")}
*/ */
module.exports = { module.exports = {
...require('./server'), get server() {
if (!serverModule) {
serverModule = require('./server')
}
return serverModule
},
...obj, ...obj,
} }
Object.assign(module.exports, require('./server'))

View File

@ -4,6 +4,7 @@ const createOption = require('../util/option.js')
module.exports = (query, request) => { module.exports = (query, request) => {
const data = { const data = {
ctcode: query.ctcode || '86', ctcode: query.ctcode || '86',
secrete: 'music_middleuser_pclogin',
cellphone: query.phone, cellphone: query.phone,
} }
return request(`/api/sms/captcha/sent`, data, createOption(query, 'weapi')) return request(`/api/sms/captcha/sent`, data, createOption(query, 'weapi'))

View File

@ -13,7 +13,7 @@ module.exports = async (query, request) => {
[query.captcha ? 'captcha' : 'password']: query.captcha [query.captcha ? 'captcha' : 'password']: query.captcha
? query.captcha ? query.captcha
: query.md5_password || CryptoJS.MD5(query.password).toString(), : query.md5_password || CryptoJS.MD5(query.password).toString(),
rememberLogin: 'true', remember: 'true',
} }
let result = await request( let result = await request(
`/api/w/login/cellphone`, `/api/w/login/cellphone`,

View File

@ -1,9 +1,20 @@
const QRCode = require('qrcode') const QRCode = require('qrcode')
const { generateChainId } = require('../util/index')
const createOption = require('../util/option.js') module.exports = (query) => {
module.exports = (query, request) => {
return new Promise(async (resolve) => { return new Promise(async (resolve) => {
const url = `https://music.163.com/login?codekey=${query.key}` const platform = query.platform || 'pc'
const cookie = query.cookie || ''
// 构建基础URL
let url = `https://music.163.com/login?codekey=${query.key}`
// 如果是web平台则添加chainId参数
if (platform === 'web') {
const chainId = generateChainId(cookie)
url += `&chainId=${chainId}`
}
return resolve({ return resolve({
code: 200, code: 200,
status: 200, status: 200,

View File

@ -0,0 +1,11 @@
// 歌单分类列表
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {
cat: query.cat || '全部',
limit: query.limit || 24,
newStyle: true,
}
return request(`/api/playlist/category/list`, data, createOption(query))
}

View File

@ -2,5 +2,5 @@
const createOption = require('../util/option.js') const createOption = require('../util/option.js')
module.exports = (query, request) => { module.exports = (query, request) => {
return request(`/api/playlist/catalogue`, {}, createOption(query, 'weapi')) return request(`/api/playlist/catalogue`, {}, createOption(query, 'eapi'))
} }

View File

@ -1,10 +1,14 @@
// 收藏与取消收藏歌单 // 收藏与取消收藏歌单
const { APP_CONF } = require('../util/config.json')
const createOption = require('../util/option.js') const createOption = require('../util/option.js')
module.exports = (query, request) => { module.exports = (query, request) => {
query.t = query.t == 1 ? 'subscribe' : 'unsubscribe' const path = query.t == 1 ? 'subscribe' : 'unsubscribe'
const data = { const data = {
id: query.id, id: query.id,
...(query.t === 1
? { checkToken: query.checkToken || APP_CONF.checkToken }
: {}),
} }
return request(`/api/playlist/${query.t}`, data, createOption(query, 'weapi')) query.checkToken = true // 强制开启checkToken
return request(`/api/playlist/${path}`, data, createOption(query, 'eapi'))
} }

View File

@ -2,17 +2,13 @@ const CryptoJS = require('crypto-js')
const path = require('path') const path = require('path')
const fs = require('fs') const fs = require('fs')
const ID_XOR_KEY_1 = '3go8&$8*3*3h0k(2)2' const ID_XOR_KEY_1 = '3go8&$8*3*3h0k(2)2'
const deviceidText = fs.readFileSync(
path.resolve(__dirname, '../data/deviceid.txt'),
'utf-8',
)
const createOption = require('../util/option.js') const createOption = require('../util/option.js')
const deviceidList = deviceidText.split('\n') const { generateDeviceId } = require('../util/index')
function getRandomFromList(list) { // function getRandomFromList(list) {
return list[Math.floor(Math.random() * list.length)] // return list[Math.floor(Math.random() * list.length)]
} // }
function cloudmusic_dll_encode_id(some_id) { function cloudmusic_dll_encode_id(some_id) {
let xoredString = '' let xoredString = ''
for (let i = 0; i < some_id.length; i++) { for (let i = 0; i < some_id.length; i++) {
@ -26,7 +22,8 @@ function cloudmusic_dll_encode_id(some_id) {
} }
module.exports = async (query, request) => { module.exports = async (query, request) => {
const deviceId = getRandomFromList(deviceidList) const deviceId = generateDeviceId()
console.log(`[register_anonimous] deviceId: ${deviceId}`)
global.deviceId = deviceId global.deviceId = deviceId
const encodedId = CryptoJS.enc.Base64.stringify( const encodedId = CryptoJS.enc.Base64.stringify(
CryptoJS.enc.Utf8.parse( CryptoJS.enc.Utf8.parse(

View File

@ -9,6 +9,7 @@ module.exports = (query, request) => {
password: CryptoJS.MD5(query.password).toString(), password: CryptoJS.MD5(query.password).toString(),
nickname: query.nickname, nickname: query.nickname,
countrycode: query.countrycode || '86', countrycode: query.countrycode || '86',
force: 'false',
} }
return request(`/api/register/cellphone`, data, createOption(query)) return request(`/api/w/register/cellphone`, data, createOption(query))
} }

View File

@ -0,0 +1,6 @@
// 所有榜单内容摘要v2
const createOption = require('../util/option.js')
module.exports = (query, request) => {
return request(`/api/toplist/detail/v2`, {}, createOption(query, 'weapi'))
}

20
module/user_detail_new.js Normal file
View File

@ -0,0 +1,20 @@
// 用户详情
const createOption = require('../util/option.js')
module.exports = async (query, request) => {
const data = {
all: 'true',
userId: query.uid,
}
const res = await request(
`/api/w/v1/user/detail/${query.uid}`,
data,
createOption(query, 'eapi'),
)
// const result = JSON.stringify(res).replace(
// /avatarImgId_str/g,
// "avatarImgIdStr"
// );
// return JSON.parse(result);
return res
}

View File

@ -1,6 +1,6 @@
{ {
"name": "NeteaseCloudMusicApi", "name": "NeteaseCloudMusicApi",
"version": "4.25.0", "version": "4.28.0",
"description": "网易云音乐 NodeJS 版 API", "description": "网易云音乐 NodeJS 版 API",
"scripts": { "scripts": {
"start": "node app.js", "start": "node app.js",
@ -65,9 +65,7 @@
"data" "data"
], ],
"dependencies": { "dependencies": {
"@unblockneteasemusic/server": "latest",
"axios": "^1.2.2", "axios": "^1.2.2",
"dotenv": "^16.0.3",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"express": "^4.17.1", "express": "^4.17.1",
"express-fileupload": "^1.1.9", "express-fileupload": "^1.1.9",

View File

@ -8,6 +8,19 @@
<link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet"> <link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet">
</head> </head>
<style>
.decode-result {
white-space: pre-wrap;
word-break: break-all;
background-color: #f0f0f0;
padding: 10px;
border-radius: 5px;
margin-top: 10px;
height: 300px;
overflow: auto;
}
</style>
<body> <body>
<div id="app" class="p-5 flex flex-col"> <div id="app" class="p-5 flex flex-col">
<h1 class="text-2xl font-bold mb-5">eapi 参数和返回内容解析</h1> <h1 class="text-2xl font-bold mb-5">eapi 参数和返回内容解析</h1>
@ -23,7 +36,7 @@
</div> </div>
<div> <div>
<p>解密结果: <p>解密结果:
<pre>{{ JSON.stringify(JSON.parse(result), null, 2) }}</pre> <pre class="decode-result">{{ JSON.stringify(JSON.parse(result), null, 2) }}</pre>
</p> </p>
</div> </div>

View File

@ -85,7 +85,6 @@
<body> <body>
<div class="container"> <div class="container">
<h1>网易云音乐 API <span id="api-version"></span></h1> <h1>网易云音乐 API <span id="api-version"></span></h1>
<p>本项目基于binaryify的网易云API二改, 添加了解灰接口</p>
<p>当你看到这个页面时,这个服务已经成功跑起来了~</p> <p>当你看到这个页面时,这个服务已经成功跑起来了~</p>
<p class="current-url"><span id="current-url"></span></p> <p class="current-url"><span id="current-url"></span></p>
<a href="/docs">查看文档</a> <a href="/docs">查看文档</a>

View File

@ -24,7 +24,7 @@
}) })
const key = res.data.data.unikey const key = res.data.data.unikey
const res2 = await axios({ const res2 = await axios({
url: `/login/qr/create?key=${key}&qrimg=true&timestamp=${Date.now()}`, url: `/login/qr/create?key=${key}&platform=web&qrimg=true&timestamp=${Date.now()}`,
}) })
document.querySelector('#qrImg').src = res2.data.data.qrimg document.querySelector('#qrImg').src = res2.data.data.qrimg

View File

@ -23,7 +23,7 @@
}) })
const key = res.data.data.unikey const key = res.data.data.unikey
const res2 = await axios({ const res2 = await axios({
url: `/login/qr/create?key=${key}&qrimg=true&timestamp=${Date.now()}`, url: `/login/qr/create?key=${key}&platform=web&qrimg=true&timestamp=${Date.now()}`,
}) })
document.querySelector('#qrImg').src = res2.data.data.qrimg document.querySelector('#qrImg').src = res2.data.data.qrimg

View File

@ -224,8 +224,8 @@ async function consturctServer(moduleDefs) {
const obj = [...params] const obj = [...params]
let ip = req.ip let ip = req.ip
if (ip.substr(0, 7) == '::ffff:') { if (ip.substring(0, 7) == '::ffff:') {
ip = ip.substr(7) ip = ip.substring(7)
} }
if (ip == '::1') { if (ip == '::1') {
ip = global.cnIp ip = global.cnIp

169
util/client-sign.js Normal file
View File

@ -0,0 +1,169 @@
const crypto = require("crypto");
const os = require("os");
class AdvancedClientSignGenerator {
/**
* 获取本机MAC地址
*/
static getRealMacAddress() {
try {
const interfaces = os.networkInterfaces();
for (let interfaceName in interfaces) {
const interface = interfaces[interfaceName];
for (let i = 0; i < interface.length; i++) {
const alias = interface[i];
// 排除内部地址和无效地址
if (
alias.mac &&
alias.mac !== "00:00:00:00:00:00" &&
!alias.internal
) {
return alias.mac.toUpperCase();
}
}
}
return null;
} catch (error) {
console.warn("获取MAC地址失败:", error.message);
return null;
}
}
/**
* 生成随机MAC地址
*/
static generateRandomMac() {
const chars = "0123456789ABCDEF";
let mac = "";
for (let i = 0; i < 6; i++) {
if (i > 0) mac += ":";
mac +=
chars[Math.floor(Math.random() * 16)] +
chars[Math.floor(Math.random() * 16)];
}
// 确保第一个字节是单播地址最低位为0
const firstByte = parseInt(mac.substring(0, 2), 16);
const unicastFirstByte = (firstByte & 0xfe)
.toString(16)
.padStart(2, "0")
.toUpperCase();
return unicastFirstByte + mac.substring(2);
}
/**
* 获取MAC地址优先真实否则随机
*/
static getMacAddress() {
const realMac = this.getRealMacAddress();
if (realMac) {
return realMac;
}
console.warn("无法获取真实MAC地址使用随机生成");
return this.generateRandomMac();
}
/**
* 字符串转HEX编码
*/
static stringToHex(str) {
return Buffer.from(str, "utf8").toString("hex").toUpperCase();
}
/**
* SHA-256哈希
*/
static sha256(data) {
return crypto.createHash("sha256").update(data, "utf8").digest("hex");
}
/**
* 生成随机设备ID
*/
static generateRandomDeviceId() {
const partLengths = [4, 4, 4, 4, 4, 4, 4, 5]; // 各部分长度
const chars = "0123456789ABCDEF";
const parts = partLengths.map((length) => {
let part = "";
for (let i = 0; i < length; i++) {
part += chars[Math.floor(Math.random() * 16)];
}
return part;
});
return parts.join("_");
}
/**
* 生成随机clientSign优先使用真实MAC否则随机
*/
static generateRandomClientSign(secretKey = "") {
// 获取MAC地址优先真实否则随机
const macAddress = this.getMacAddress();
// 生成随机设备ID
const deviceId = this.generateRandomDeviceId();
// 转换设备ID为HEX
const hexDeviceId = this.stringToHex(deviceId);
// 构造签名字符串
const signString = `${macAddress}@@@${hexDeviceId}`;
// 生成哈希
const hash = this.sha256(signString + secretKey);
return `${signString}@@@@@@${hash}`;
}
/**
* 批量生成多个随机签名
*/
static generateMultipleRandomSigns(count, secretKey = "") {
const signs = [];
for (let i = 0; i < count; i++) {
signs.push(this.generateRandomClientSign(secretKey));
}
return signs;
}
/**
* 使用指定参数生成签名
*/
static generateWithCustomDeviceId(macAddress, deviceId, secretKey = "") {
const hexDeviceId = this.stringToHex(deviceId);
const signString = `${macAddress}@@@${hexDeviceId}`;
const hash = this.sha256(signString + secretKey);
return `${signString}@@@@@@${hash}`;
}
/**
* 验证签名格式是否正确
*/
static validateClientSign(clientSign) {
try {
const parts = clientSign.split("@@@@@@");
if (parts.length !== 2) return false;
const [infoPart, hash] = parts;
const infoParts = infoPart.split("@@@");
if (infoParts.length !== 2) return false;
const [mac, hexDeviceId] = infoParts;
// 验证MAC地址格式
const macRegex = /^([0-9A-F]{2}:){5}[0-9A-F]{2}$/;
if (!macRegex.test(mac)) return false;
// 验证哈希格式
const hashRegex = /^[0-9a-f]{64}$/;
if (!hashRegex.test(hash)) return false;
return true;
} catch (error) {
return false;
}
}
}
module.exports = AdvancedClientSignGenerator;

View File

@ -13,6 +13,8 @@
"apiDomain": "https://interface.music.163.com", "apiDomain": "https://interface.music.163.com",
"domain": "https://music.163.com", "domain": "https://music.163.com",
"encrypt": true, "encrypt": true,
"encryptResponse": false "encryptResponse": false,
"clientSign": "18:C0:4D:B9:8F:FE@@@453832335F384641365F424635335F303030315F303031425F343434415F343643365F333638332@@@@@@6ff673ef74955b38bce2fa8562d95c976ed4758b1227c4e9ee345987cee17bc9",
"checkToken": "9ca17ae2e6ffcda170e2e6ee8af14fbabdb988f225b3868eb2c15a879b9a83d274a790ac8ff54a97b889d5d42af0feaec3b92af58cff99c470a7eafd88f75e839a9ea7c14e909da883e83fb692a3abdb6b92adee9e"
} }
} }

View File

@ -87,8 +87,13 @@ const eapi = (url, object) => {
} }
const eapiResDecrypt = (encryptedParams) => { const eapiResDecrypt = (encryptedParams) => {
// 使用aesDecrypt解密参数 // 使用aesDecrypt解密参数
try {
const decryptedData = aesDecrypt(encryptedParams, eapiKey, '', 'hex') const decryptedData = aesDecrypt(encryptedParams, eapiKey, '', 'hex')
return JSON.parse(decryptedData) return JSON.parse(decryptedData)
} catch (error) {
console.log('eapiResDecrypt error:', error)
return null
}
} }
const eapiReqDecrypt = (encryptedParams) => { const eapiReqDecrypt = (encryptedParams) => {
// 使用aesDecrypt解密参数 // 使用aesDecrypt解密参数

View File

@ -1,49 +1,156 @@
// 预先定义常量和函数引用
const chinaIPPrefixes = [
'116.25',
'116.76',
'116.77',
'116.78',
'116.79',
'116.80',
'116.81',
'116.82',
'116.83',
'116.84',
'116.85',
'116.86',
'116.87',
'116.88',
'116.89',
'116.90',
'116.91',
'116.92',
'116.93',
'116.94',
]
const prefixesLength = chinaIPPrefixes.length
const floor = Math.floor
const random = Math.random
const keys = Object.keys
// 预编译encodeURIComponent以减少查找开销
const encode = encodeURIComponent
module.exports = { module.exports = {
toBoolean(val) { toBoolean(val) {
if (typeof val === 'boolean') return val if (typeof val === 'boolean') return val
if (val === '') return val if (val === '') return val
return val === 'true' || val == '1' return val === 'true' || val == '1'
}, },
cookieToJson(cookie) { cookieToJson(cookie) {
if (!cookie) return {} if (!cookie) return {}
let cookieArr = cookie.split(';') let cookieArr = cookie.split(';')
let obj = {} let obj = {}
cookieArr.forEach((i) => {
let arr = i.split('=') // 优化使用for循环替代forEach性能更好
if (arr.length == 2) obj[arr[0].trim()] = arr[1].trim() for (let i = 0, len = cookieArr.length; i < len; i++) {
}) let item = cookieArr[i]
let arr = item.split('=')
// 优化:使用严格等于
if (arr.length === 2) {
obj[arr[0].trim()] = arr[1].trim()
}
}
return obj return obj
}, },
cookieObjToString(cookie) {
return Object.keys(cookie)
.map(
(key) =>
`${encodeURIComponent(key)}=${encodeURIComponent(cookie[key])}`,
)
.join('; ')
},
getRandom(num) {
var random = Math.floor(
(Math.random() + Math.floor(Math.random() * 9 + 1)) *
Math.pow(10, num - 1),
)
return random
},
generateRandomChineseIP() {
const chinaIPPrefixes = ['116.25', '116.76', '116.77', '116.78']
const randomPrefix = cookieObjToString(cookie) {
chinaIPPrefixes[Math.floor(Math.random() * chinaIPPrefixes.length)] // 优化使用预绑定的keys函数和for循环
const cookieKeys = keys(cookie)
const result = []
// 优化使用for循环和预分配数组
for (let i = 0, len = cookieKeys.length; i < len; i++) {
const key = cookieKeys[i]
result[i] = `${encode(key)}=${encode(cookie[key])}`
}
return result.join('; ')
},
getRandom(num) {
// 优化:简化随机数生成逻辑
// 原逻辑看起来有问题,这里保持原意但优化性能
var randomValue = random()
var floorValue = floor(randomValue * 9 + 1)
var powValue = Math.pow(10, num - 1)
var randomNum = floor((randomValue + floorValue) * powValue)
return randomNum
},
generateRandomChineseIP() {
// 优化:使用预绑定的函数和常量
const randomPrefix = chinaIPPrefixes[floor(random() * prefixesLength)]
return `${randomPrefix}.${generateIPSegment()}.${generateIPSegment()}` return `${randomPrefix}.${generateIPSegment()}.${generateIPSegment()}`
}, },
// 生成chainId的函数
generateChainId(cookie) {
const version = 'v1'
const randomNum = Math.floor(Math.random() * 1e6)
const deviceId =
getCookieValue(cookie, 'sDeviceId') || 'unknown-' + randomNum
const platform = 'web'
const action = 'login'
const timestamp = Date.now()
return `${version}_${deviceId}_${platform}_${action}_${timestamp}`
},
generateDeviceId() {
const hexChars = '0123456789ABCDEF'
const chars = []
for (let i = 0; i < 52; i++) {
const randomIndex = Math.floor(Math.random() * hexChars.length)
chars.push(hexChars[randomIndex])
}
return chars.join('')
},
} }
// 生成一个随机整数 // 优化:预先绑定函
function getRandomInt(min, max) { function getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min // 优化:简化计算
return floor(random() * (max - min + 1)) + min
} }
// 生成一个随机IP地址段 // 优化预先绑定generateIPSegment函数引用
function generateIPSegment() { function generateIPSegment() {
// 优化:内联常量
return getRandomInt(1, 255) return getRandomInt(1, 255)
} }
// 进一步优化版本(如果需要更高性能):
/*
const cookieToJsonOptimized = (function() {
// 预编译trim函数
const trim = String.prototype.trim
return function(cookie) {
if (!cookie) return {}
const cookieArr = cookie.split(';')
const obj = {}
for (let i = 0, len = cookieArr.length; i < len; i++) {
const item = cookieArr[i]
const eqIndex = item.indexOf('=')
if (eqIndex > 0 && eqIndex < item.length - 1) {
const key = trim.call(item.substring(0, eqIndex))
const value = trim.call(item.substring(eqIndex + 1))
obj[key] = value
}
}
return obj
}
})()
*/
// 用于从cookie字符串中获取指定值的辅助函数
function getCookieValue(cookieStr, name) {
if (!cookieStr) return ''
const cookies = '; ' + cookieStr
const parts = cookies.split('; ' + name + '=')
if (parts.length === 2) return parts.pop().split(';').shift()
return ''
}

View File

@ -1,63 +1,71 @@
function MemoryCache() { class MemoryCache {
this.cache = {} constructor() {
this.cache = new Map()
this.size = 0 this.size = 0
} }
MemoryCache.prototype.add = function (key, value, time, timeoutCallback) { add(key, value, time, timeoutCallback) {
var old = this.cache[key] // 移除旧的条目(如果存在)
var instance = this const old = this.cache.get(key)
if (old) {
clearTimeout(old.timeout)
}
var entry = { // 创建新的缓存条目
value: value, const entry = {
value,
expire: time + Date.now(), expire: time + Date.now(),
timeout: setTimeout(function () { timeout: setTimeout(() => {
instance.delete(key) this.delete(key)
return ( if (typeof timeoutCallback === 'function') {
timeoutCallback &&
typeof timeoutCallback === 'function' &&
timeoutCallback(value, key) timeoutCallback(value, key)
) }
}, time), }, time),
} }
this.cache[key] = entry this.cache.set(key, entry)
this.size = Object.keys(this.cache).length this.size = this.cache.size
return entry return entry
}
MemoryCache.prototype.delete = function (key) {
var entry = this.cache[key]
if (entry) {
clearTimeout(entry.timeout)
} }
delete this.cache[key] delete(key) {
const entry = this.cache.get(key)
this.size = Object.keys(this.cache).length if (entry) {
clearTimeout(entry.timeout)
this.cache.delete(key)
this.size = this.cache.size
}
return null return null
} }
MemoryCache.prototype.get = function (key) { get(key) {
var entry = this.cache[key] return this.cache.get(key) || null
}
return entry getValue(key) {
} const entry = this.cache.get(key)
return entry ? entry.value : undefined
}
MemoryCache.prototype.getValue = function (key) { clear() {
var entry = this.get(key) this.cache.forEach((entry) => clearTimeout(entry.timeout))
this.cache.clear()
this.size = 0
return true
}
return entry && entry.value has(key) {
} const entry = this.cache.get(key)
if (!entry) return false
MemoryCache.prototype.clear = function () { if (Date.now() > entry.expire) {
Object.keys(this.cache).forEach(function (key) {
this.delete(key) this.delete(key)
}, this) return false
}
return true return true
}
} }
module.exports = MemoryCache module.exports = MemoryCache

View File

@ -6,6 +6,8 @@ const createOption = (query, crypto = '') => {
proxy: query.proxy, proxy: query.proxy,
realIP: query.realIP, realIP: query.realIP,
e_r: query.e_r || undefined, e_r: query.e_r || undefined,
domain: query.domain || '',
checkToken: query.checkToken || false,
} }
} }
module.exports = createOption module.exports = createOption

View File

@ -1,3 +1,4 @@
// 预先导入和绑定常用模块及函数
const encrypt = require('./crypto') const encrypt = require('./crypto')
const CryptoJS = require('crypto-js') const CryptoJS = require('crypto-js')
const { default: axios } = require('axios') const { default: axios } = require('axios')
@ -8,30 +9,50 @@ const tunnel = require('tunnel')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const tmpPath = require('os').tmpdir() const tmpPath = require('os').tmpdir()
const { cookieToJson, cookieObjToString, toBoolean } = require('./index') const {
cookieToJson,
cookieObjToString,
toBoolean,
generateRandomChineseIP,
} = require('./index')
const { URLSearchParams, URL } = require('url')
const { APP_CONF } = require('../util/config.json')
// 预先读取匿名token并缓存
const anonymous_token = fs.readFileSync( const anonymous_token = fs.readFileSync(
path.resolve(tmpPath, './anonymous_token'), path.resolve(tmpPath, './anonymous_token'),
'utf-8', 'utf-8',
) )
const { URLSearchParams, URL } = require('url')
const { APP_CONF } = require('../util/config.json')
// request.debug = true // 开启可看到更详细信息
// 预先绑定常用函数和常量
const floor = Math.floor
const random = Math.random
const now = Date.now
const keys = Object.keys
const stringify = JSON.stringify
const parse = JSON.parse
const characters = 'abcdefghijklmnopqrstuvwxyz'
const charactersLength = characters.length
// 预先创建HTTP/HTTPS agents并重用
const createHttpAgent = () => new http.Agent({ keepAlive: true })
const createHttpsAgent = () => new https.Agent({ keepAlive: true })
// 预先计算WNMCID只计算一次
const WNMCID = (function () { const WNMCID = (function () {
const characters = 'abcdefghijklmnopqrstuvwxyz'
let randomString = '' let randomString = ''
for (let i = 0; i < 6; i++) for (let i = 0; i < 6; i++) {
randomString += characters.charAt( randomString += characters.charAt(floor(random() * charactersLength))
Math.floor(Math.random() * characters.length), }
) return `${randomString}.${now().toString()}.01.0`
return `${randomString}.${Date.now().toString()}.01.0`
})() })()
// 预先定义osMap
const osMap = { const osMap = {
pc: { pc: {
os: 'pc', os: 'pc',
appver: '3.0.18.203152', appver: '3.1.17.204416',
osver: 'Microsoft-Windows-10-Professional-build-22631-64bit', osver: 'Microsoft-Windows-10-Professional-build-19045-64bit',
channel: 'netease', channel: 'netease',
}, },
linux: { linux: {
@ -54,8 +75,8 @@ const osMap = {
}, },
} }
const chooseUserAgent = (crypto, uaType = 'pc') => { // 预先定义userAgentMap
const userAgentMap = { const userAgentMap = {
weapi: { weapi: {
pc: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 Edg/124.0.0.0', pc: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 Edg/124.0.0.0',
}, },
@ -69,76 +90,113 @@ const chooseUserAgent = (crypto, uaType = 'pc') => {
'NeteaseMusic/9.1.65.240927161425(9001065);Dalvik/2.1.0 (Linux; U; Android 14; 23013RK75C Build/UKQ1.230804.001)', 'NeteaseMusic/9.1.65.240927161425(9001065);Dalvik/2.1.0 (Linux; U; Android 14; 23013RK75C Build/UKQ1.230804.001)',
iphone: 'NeteaseMusic 9.0.90/5038 (iPhone; iOS 16.2; zh_CN)', iphone: 'NeteaseMusic 9.0.90/5038 (iPhone; iOS 16.2; zh_CN)',
}, },
}
return userAgentMap[crypto][uaType] || ''
} }
const createRequest = (uri, data, options) => {
return new Promise((resolve, reject) => {
let headers = options.headers || {}
let ip = options.realIP || options.ip || ''
// console.log(ip)
if (ip) {
headers['X-Real-IP'] = ip
headers['X-Forwarded-For'] = ip
}
// headers['X-Real-IP'] = '118.88.88.88'
let cookie = options.cookie || {} // 预先定义常量
if (typeof cookie === 'string') { const DOMAIN = APP_CONF.domain
cookie = cookieToJson(cookie) const API_DOMAIN = APP_CONF.apiDomain
} const ENCRYPT_RESPONSE = APP_CONF.encryptResponse
if (typeof cookie === 'object') { const SPECIAL_STATUS_CODES = new Set([201, 302, 400, 502, 800, 801, 802, 803])
let _ntes_nuid = CryptoJS.lib.WordArray.random(32).toString()
let os = osMap[cookie.os] || osMap['iphone'] // chooseUserAgent函数
cookie = { const chooseUserAgent = (crypto, uaType = 'pc') => {
return userAgentMap[crypto]?.[uaType] || ''
}
// cookie处理
const processCookieObject = (cookie, uri) => {
const _ntes_nuid = CryptoJS.lib.WordArray.random(32).toString()
const os = osMap[cookie.os] || osMap['pc']
const processedCookie = {
...cookie, ...cookie,
__remember_me: 'true', __remember_me: 'true',
// NMTID: CryptoJS.lib.WordArray.random(16).toString(),
ntes_kaola_ad: '1', ntes_kaola_ad: '1',
_ntes_nuid: cookie._ntes_nuid || _ntes_nuid, _ntes_nuid: cookie._ntes_nuid || _ntes_nuid,
_ntes_nnid: _ntes_nnid: cookie._ntes_nnid || `${_ntes_nuid},${now().toString()}`,
cookie._ntes_nnid || `${_ntes_nuid},${Date.now().toString()}`,
WNMCID: cookie.WNMCID || WNMCID, WNMCID: cookie.WNMCID || WNMCID,
WEVNSM: cookie.WEVNSM || '1.0.0', WEVNSM: cookie.WEVNSM || '1.0.0',
osver: cookie.osver || os.osver, osver: cookie.osver || os.osver,
deviceId: cookie.deviceId || global.deviceId, deviceId: cookie.deviceId || global.deviceId,
os: cookie.os || os.os, os: cookie.os || os.os,
channel: cookie.channel || os.channel, channel: cookie.channel || os.channel,
appver: cookie.appver || os.appver, appver: cookie.appver || os.appver,
} }
if (uri.indexOf('login') === -1) { if (uri.indexOf('login') === -1) {
cookie['NMTID'] = CryptoJS.lib.WordArray.random(16).toString() processedCookie['NMTID'] = CryptoJS.lib.WordArray.random(16).toString()
} }
if (!cookie.MUSIC_U) {
// 游客 if (!processedCookie.MUSIC_U) {
cookie.MUSIC_A = cookie.MUSIC_A || anonymous_token processedCookie.MUSIC_A = processedCookie.MUSIC_A || anonymous_token
} }
return processedCookie
}
// header cookie生成
const createHeaderCookie = (header) => {
const headerKeys = keys(header)
const cookieParts = new Array(headerKeys.length)
for (let i = 0, len = headerKeys.length; i < len; i++) {
const key = headerKeys[i]
cookieParts[i] =
encodeURIComponent(key) + '=' + encodeURIComponent(header[key])
}
return cookieParts.join('; ')
}
// requestId生成
const generateRequestId = () => {
return `${now()}_${floor(random() * 1000)
.toString()
.padStart(4, "0")}`;
}
const createRequest = (uri, data, options) => {
return new Promise((resolve, reject) => {
// 变量声明和初始化
const headers = options.headers ? { ...options.headers } : {}
const ip = options.realIP || options.ip || ''
// IP头设置
if (ip) {
headers['X-Real-IP'] = ip
headers['X-Forwarded-For'] = ip
}
let cookie = options.cookie || {}
if (typeof cookie === 'string') {
cookie = cookieToJson(cookie)
}
if (typeof cookie === 'object') {
cookie = processCookieObject(cookie, uri)
headers['Cookie'] = cookieObjToString(cookie) headers['Cookie'] = cookieObjToString(cookie)
} }
let url = ''
let encryptData = ''
let crypto = options.crypto
const csrfToken = cookie['__csrf'] || ''
let url = '', // 加密方式选择
encryptData = '',
crypto = options.crypto,
csrfToken = cookie['__csrf'] || ''
if (crypto === '') { if (crypto === '') {
// 加密方式为空,以配置文件的加密方式为准 crypto = APP_CONF.encrypt ? 'eapi' : 'api'
if (APP_CONF.encrypt) {
crypto = 'eapi'
} else {
crypto = 'api'
}
} }
// 根据加密方式加密请求数据目前任意uri都支持四种加密方式 const answer = { status: 500, body: {}, cookie: [] }
// 根据加密方式处理
switch (crypto) { switch (crypto) {
case 'weapi': case 'weapi':
headers['Referer'] = APP_CONF.domain headers['Referer'] = DOMAIN
headers['User-Agent'] = options.ua || chooseUserAgent('weapi') headers['User-Agent'] = options.ua || chooseUserAgent('weapi')
data.csrf_token = csrfToken data.csrf_token = csrfToken
encryptData = encrypt.weapi(data) encryptData = encrypt.weapi(data)
url = APP_CONF.domain + '/weapi/' + uri.substr(5) url = DOMAIN + '/weapi/' + uri.substr(5)
break break
case 'linuxapi': case 'linuxapi':
@ -146,93 +204,89 @@ const createRequest = (uri, data, options) => {
options.ua || chooseUserAgent('linuxapi', 'linux') options.ua || chooseUserAgent('linuxapi', 'linux')
encryptData = encrypt.linuxapi({ encryptData = encrypt.linuxapi({
method: 'POST', method: 'POST',
url: APP_CONF.domain + uri, url: DOMAIN + uri,
params: data, params: data,
}) })
url = APP_CONF.domain + '/api/linux/forward' url = DOMAIN + '/api/linux/forward'
break break
case 'eapi': case 'eapi':
case 'api': case 'api':
// 两种加密方式都应生成客户端的cookie // header创建
const header = { const header = {
osver: cookie.osver, //系统版本 osver: cookie.osver,
deviceId: cookie.deviceId, deviceId: cookie.deviceId,
os: cookie.os, //系统类型 os: cookie.os,
appver: cookie.appver, // app版本 appver: cookie.appver,
versioncode: cookie.versioncode || '140', //版本号 versioncode: cookie.versioncode || '140',
mobilename: cookie.mobilename || '', //设备model mobilename: cookie.mobilename || '',
buildver: cookie.buildver || Date.now().toString().substr(0, 10), buildver: cookie.buildver || now().toString().substr(0, 10),
resolution: cookie.resolution || '1920x1080', //设备分辨率 resolution: cookie.resolution || '1920x1080',
__csrf: csrfToken, __csrf: csrfToken,
channel: cookie.channel, //下载渠道 channel: cookie.channel,
requestId: `${Date.now()}_${Math.floor(Math.random() * 1000) requestId: generateRequestId(),
.toString() ...(options.checkToken
.padStart(4, '0')}`, ? { 'X-antiCheatToken': APP_CONF.checkToken }
: {}),
// clientSign: APP_CONF.clientSign,
} }
if (cookie.MUSIC_U) header['MUSIC_U'] = cookie.MUSIC_U if (cookie.MUSIC_U) header['MUSIC_U'] = cookie.MUSIC_U
if (cookie.MUSIC_A) header['MUSIC_A'] = cookie.MUSIC_A if (cookie.MUSIC_A) header['MUSIC_A'] = cookie.MUSIC_A
headers['Cookie'] = Object.keys(header)
.map( headers['Cookie'] = createHeaderCookie(header)
(key) =>
encodeURIComponent(key) + '=' + encodeURIComponent(header[key]),
)
.join('; ')
headers['User-Agent'] = options.ua || chooseUserAgent('api', 'iphone') headers['User-Agent'] = options.ua || chooseUserAgent('api', 'iphone')
if (crypto === 'eapi') { if (crypto === 'eapi') {
// 使用eapi加密
data.header = header data.header = header
data.e_r = toBoolean( data.e_r = toBoolean(
options.e_r !== undefined options.e_r !== undefined
? options.e_r ? options.e_r
: data.e_r !== undefined : data.e_r !== undefined
? data.e_r ? data.e_r
: APP_CONF.encryptResponse, : ENCRYPT_RESPONSE,
) // 用于加密接口返回值 )
encryptData = encrypt.eapi(uri, data) encryptData = encrypt.eapi(uri, data)
url = APP_CONF.apiDomain + '/eapi/' + uri.substr(5) url = API_DOMAIN + '/eapi/' + uri.substr(5)
} else if (crypto === 'api') { } else if (crypto === 'api') {
// 不使用任何加密 url = API_DOMAIN + uri
url = APP_CONF.apiDomain + uri
encryptData = data encryptData = data
} }
break break
default: default:
// 未知的加密方式
console.log('[ERR]', 'Unknown Crypto:', crypto) console.log('[ERR]', 'Unknown Crypto:', crypto)
break break
} }
const answer = { status: 500, body: {}, cookie: [] } // console.log(url);
// console.log(headers, 'headers') // settings创建
let settings = { let settings = {
method: 'POST', method: 'POST',
url: url, url: url,
headers: headers, headers: headers,
data: new URLSearchParams(encryptData).toString(), data: new URLSearchParams(encryptData).toString(),
httpAgent: new http.Agent({ keepAlive: true }), httpAgent: createHttpAgent(),
httpsAgent: new https.Agent({ keepAlive: true }), httpsAgent: createHttpsAgent(),
} }
// e_r处理
if (data.e_r) { if (data.e_r) {
settings = { settings.encoding = null
...settings, settings.responseType = 'arraybuffer'
encoding: null,
responseType: 'arraybuffer',
}
} }
// 代理处理
if (options.proxy) { if (options.proxy) {
if (options.proxy.indexOf('pac') > -1) { if (options.proxy.indexOf('pac') > -1) {
settings.httpAgent = new PacProxyAgent(options.proxy) const agent = new PacProxyAgent(options.proxy)
settings.httpsAgent = new PacProxyAgent(options.proxy) settings.httpAgent = agent
settings.httpsAgent = agent
} else { } else {
try {
const purl = new URL(options.proxy) const purl = new URL(options.proxy)
if (purl.hostname) { if (purl.hostname) {
const agent = tunnel[ const isHttps = purl.protocol === 'https:'
purl.protocol === 'https' ? 'httpsOverHttp' : 'httpOverHttp' const agent = tunnel[isHttps ? 'httpsOverHttp' : 'httpOverHttp']({
]({
proxy: { proxy: {
host: purl.hostname, host: purl.hostname,
port: purl.port || 80, port: purl.port || 80,
@ -248,25 +302,29 @@ const createRequest = (uri, data, options) => {
} else { } else {
console.error('代理配置无效,不使用代理') console.error('代理配置无效,不使用代理')
} }
} catch (e) {
console.error('代理URL解析失败:', e.message)
}
} }
} else { } else {
settings.proxy = false settings.proxy = false
} }
// console.log(settings.headers);
axios(settings) axios(settings)
.then((res) => { .then((res) => {
const body = res.data const body = res.data
answer.cookie = (res.headers['set-cookie'] || []).map((x) => answer.cookie = (res.headers['set-cookie'] || []).map((x) =>
x.replace(/\s*Domain=[^(;|$)]+;*/, ''), x.replace(/\s*Domain=[^(;|$)]+;*/, ''),
) )
try { try {
if (crypto === 'eapi' && data.e_r) { if (crypto === 'eapi' && data.e_r) {
// eapi接口返回值被加密需要解密
answer.body = encrypt.eapiResDecrypt( answer.body = encrypt.eapiResDecrypt(
body.toString('hex').toUpperCase(), body.toString('hex').toUpperCase(),
) )
} else { } else {
answer.body = answer.body =
typeof body == 'object' ? body : JSON.parse(body.toString()) typeof body === 'object' ? body : parse(body.toString())
} }
if (answer.body.code) { if (answer.body.code) {
@ -274,28 +332,30 @@ const createRequest = (uri, data, options) => {
} }
answer.status = Number(answer.body.code || res.status) answer.status = Number(answer.body.code || res.status)
if (
[201, 302, 400, 502, 800, 801, 802, 803].indexOf(answer.body.code) > // 状态码检查使用Set提升查找性能
-1 if (SPECIAL_STATUS_CODES.has(answer.body.code)) {
) {
// 特殊状态码
answer.status = 200 answer.status = 200
} }
} catch (e) { } catch (e) {
// console.log(e)
// can't decrypt and can't parse directly
answer.body = body answer.body = body
answer.status = res.status answer.status = res.status
} }
answer.status = answer.status =
100 < answer.status && answer.status < 600 ? answer.status : 400 answer.status > 100 && answer.status < 600 ? answer.status : 400
if (answer.status === 200) resolve(answer)
else reject(answer) if (answer.status === 200) {
resolve(answer)
} else {
console.log('[ERR]', answer)
reject(answer)
}
}) })
.catch((err) => { .catch((err) => {
answer.status = 502 answer.status = 502
answer.body = { code: 502, msg: err } answer.body = { code: 502, msg: err.message || err }
console.log('[ERR]', answer)
reject(answer) reject(answer)
}) })
}) })