refactor: 重构模块加载

Refactored main.js to optimize module and server loading, improving code clarity and lazy loading. Updated several dependencies in package.json to their latest versions for better security and compatibility. Updated documentation cover page and home page to reflect project branding changes.

Co-Authored-By: binaryify <binaryify@gmail.com>
This commit is contained in:
ImFurina 2025-09-07 13:08:26 +08:00
parent 4f4aa134d5
commit abd0c9ca91
13 changed files with 656 additions and 294 deletions

78
main.js
View File

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

View File

@ -66,25 +66,25 @@
],
"dependencies": {
"@unblockneteasemusic/server": "^0.27.10",
"axios": "^1.2.2",
"axios": "^1.11.0",
"crypto-js": "^4.2.0",
"dotenv": "^16.0.3",
"express": "^4.17.1",
"express-fileupload": "^1.1.9",
"dotenv": "^16.6.1",
"express": "^4.21.2",
"express-fileupload": "^1.5.2",
"md5": "^2.3.0",
"music-metadata": "^7.5.3",
"music-metadata": "^7.14.0",
"node-forge": "^1.3.1",
"pac-proxy-agent": "^7.0.0",
"qrcode": "^1.4.4",
"pac-proxy-agent": "^7.2.0",
"qrcode": "^1.5.4",
"safe-decode-uri-component": "^1.2.1",
"tunnel": "^0.0.6",
"xml2js": "^0.6.2",
"yargs": "^17.1.1"
"yargs": "^17.7.2"
},
"devDependencies": {
"@types/express": "^4.17.13",
"@types/express-fileupload": "^1.2.2",
"@types/mocha": "^9.1.0",
"@types/express": "^4.17.23",
"@types/express-fileupload": "^1.5.1",
"@types/mocha": "^9.1.1",
"@types/node": "16.11.19",
"@typescript-eslint/eslint-plugin": "5.0.0",
"@typescript-eslint/parser": "5.0.0",
@ -96,7 +96,7 @@
"intelli-espower-loader": "1.1.0",
"lint-staged": "12.1.7",
"mocha": "10.0.0",
"pkg": "^5.8.0",
"pkg": "^5.8.1",
"power-assert": "1.6.1",
"prettier": "2.7.1",
"typescript": "4.5.2"

32
pnpm-lock.yaml generated
View File

@ -12,34 +12,34 @@ importers:
specifier: ^0.27.10
version: 0.27.10
axios:
specifier: ^1.2.2
specifier: ^1.11.0
version: 1.11.0
crypto-js:
specifier: ^4.2.0
version: 4.2.0
dotenv:
specifier: ^16.0.3
specifier: ^16.6.1
version: 16.6.1
express:
specifier: ^4.17.1
specifier: ^4.21.2
version: 4.21.2
express-fileupload:
specifier: ^1.1.9
specifier: ^1.5.2
version: 1.5.2
md5:
specifier: ^2.3.0
version: 2.3.0
music-metadata:
specifier: ^7.5.3
specifier: ^7.14.0
version: 7.14.0
node-forge:
specifier: ^1.3.1
version: 1.3.1
pac-proxy-agent:
specifier: ^7.0.0
specifier: ^7.2.0
version: 7.2.0
qrcode:
specifier: ^1.4.4
specifier: ^1.5.4
version: 1.5.4
safe-decode-uri-component:
specifier: ^1.2.1
@ -51,17 +51,17 @@ importers:
specifier: ^0.6.2
version: 0.6.2
yargs:
specifier: ^17.1.1
specifier: ^17.7.2
version: 17.7.2
devDependencies:
'@types/express':
specifier: ^4.17.13
specifier: ^4.17.23
version: 4.17.23
'@types/express-fileupload':
specifier: ^1.2.2
specifier: ^1.5.1
version: 1.5.1
'@types/mocha':
specifier: ^9.1.0
specifier: ^9.1.1
version: 9.1.1
'@types/node':
specifier: 16.11.19
@ -97,7 +97,7 @@ importers:
specifier: 10.0.0
version: 10.0.0
pkg:
specifier: ^5.8.0
specifier: ^5.8.1
version: 5.8.1
power-assert:
specifier: 1.6.1
@ -1640,8 +1640,8 @@ packages:
next-tick@1.1.0:
resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==}
node-abi@3.75.0:
resolution: {integrity: sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg==}
node-abi@3.77.0:
resolution: {integrity: sha512-DSmt0OEcLoK4i3NuscSbGjOf3bqiDEutejqENSplMSFA/gmB8mkED9G4pKWnPl7MDU4rSHebKPHeitpDfyH0cQ==}
engines: {node: '>=10'}
node-fetch@2.7.0:
@ -4248,7 +4248,7 @@ snapshots:
next-tick@1.1.0: {}
node-abi@3.75.0:
node-abi@3.77.0:
dependencies:
semver: 7.7.2
@ -4534,7 +4534,7 @@ snapshots:
minimist: 1.2.8
mkdirp-classic: 0.5.3
napi-build-utils: 1.0.2
node-abi: 3.75.0
node-abi: 3.77.0
pump: 3.0.3
rc: 1.2.8
simple-get: 4.0.1

View File

@ -1,6 +1,6 @@
# 网易云音乐 API Enhanced Reborn
# 网易云音乐 API Enhanced
> 为停更的网易云音乐 NodeJs API 提供持续的维护!
> 🔍 网易云音乐API Node.js服务的复兴项目
- 基于原版网易云API新增更多有趣的功能
- 具备登录接口,多达200多个接口

View File

@ -1,6 +1,6 @@
# NeteaseCloudMusicApiEnhanced
网易云音乐 NodeJS API Enhanced(Reborn)
网易云音乐 NodeJS API Enhanced
## 灵感来自

View File

@ -146,16 +146,11 @@ async function consturctServer(moduleDefs) {
* CORS & Preflight request
*/
app.use((req, res, next) => {
// 强制设置 Access-Control-Allow-Credentials: true
if (req.path !== '/' && !req.path.includes('.')) {
let allowOrigin = CORS_ALLOW_ORIGIN || req.headers.origin
// 禁止为 *,必须为具体域名
if (!allowOrigin || allowOrigin === '*') {
allowOrigin = req.headers.origin || ''
}
res.set({
'Access-Control-Allow-Credentials': true,
'Access-Control-Allow-Origin': allowOrigin,
'Access-Control-Allow-Origin':
CORS_ALLOW_ORIGIN || req.headers.origin || '*',
'Access-Control-Allow-Headers': 'X-Requested-With,Content-Type',
'Access-Control-Allow-Methods': 'PUT,POST,GET,DELETE,OPTIONS',
'Content-Type': 'application/json; charset=utf-8',
@ -232,8 +227,8 @@ async function consturctServer(moduleDefs) {
const obj = [...params]
let ip = req.ip
if (ip.substr(0, 7) == '::ffff:') {
ip = ip.substr(7)
if (ip.substring(0, 7) == '::ffff:') {
ip = ip.substring(7)
}
if (ip == '::1') {
ip = global.cnIp
@ -359,6 +354,7 @@ async function serveNcmApi(options) {
- Node Version: ${process.version}
- Process ID: ${process.pid}`)
})
return appExt
}

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",
"domain": "https://music.163.com",
"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) => {
// 使用aesDecrypt解密参数
const decryptedData = aesDecrypt(encryptedParams, eapiKey, '', 'hex')
return JSON.parse(decryptedData)
try {
const decryptedData = aesDecrypt(encryptedParams, eapiKey, '', 'hex')
return JSON.parse(decryptedData)
} catch (error) {
console.log('eapiResDecrypt error:', error)
return null
}
}
const eapiReqDecrypt = (encryptedParams) => {
// 使用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 = {
toBoolean(val) {
if (typeof val === 'boolean') return val
if (val === '') return val
return val === 'true' || val == '1'
},
cookieToJson(cookie) {
if (!cookie) return {}
let cookieArr = cookie.split(';')
let obj = {}
cookieArr.forEach((i) => {
let arr = i.split('=')
if (arr.length == 2) obj[arr[0].trim()] = arr[1].trim()
})
// 优化使用for循环替代forEach性能更好
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
},
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 =
chinaIPPrefixes[Math.floor(Math.random() * chinaIPPrefixes.length)]
cookieObjToString(cookie) {
// 优化使用预绑定的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()}`
},
// 生成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) {
return Math.floor(Math.random() * (max - min + 1)) + min
// 优化:简化计算
return floor(random() * (max - min + 1)) + min
}
// 生成一个随机IP地址段
// 优化预先绑定generateIPSegment函数引用
function generateIPSegment() {
// 优化:内联常量
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() {
this.cache = {}
this.size = 0
}
MemoryCache.prototype.add = function (key, value, time, timeoutCallback) {
var old = this.cache[key]
var instance = this
var entry = {
value: value,
expire: time + Date.now(),
timeout: setTimeout(function () {
instance.delete(key)
return (
timeoutCallback &&
typeof timeoutCallback === 'function' &&
timeoutCallback(value, key)
)
}, time),
class MemoryCache {
constructor() {
this.cache = new Map()
this.size = 0
}
this.cache[key] = entry
this.size = Object.keys(this.cache).length
add(key, value, time, timeoutCallback) {
// 移除旧的条目(如果存在)
const old = this.cache.get(key)
if (old) {
clearTimeout(old.timeout)
}
return entry
}
// 创建新的缓存条目
const entry = {
value,
expire: time + Date.now(),
timeout: setTimeout(() => {
this.delete(key)
if (typeof timeoutCallback === 'function') {
timeoutCallback(value, key)
}
}, time),
}
MemoryCache.prototype.delete = function (key) {
var entry = this.cache[key]
this.cache.set(key, entry)
this.size = this.cache.size
if (entry) {
clearTimeout(entry.timeout)
return entry
}
delete this.cache[key]
delete(key) {
const entry = this.cache.get(key)
if (entry) {
clearTimeout(entry.timeout)
this.cache.delete(key)
this.size = this.cache.size
}
return null
}
this.size = Object.keys(this.cache).length
get(key) {
return this.cache.get(key) || null
}
return null
}
getValue(key) {
const entry = this.cache.get(key)
return entry ? entry.value : undefined
}
MemoryCache.prototype.get = function (key) {
var entry = this.cache[key]
clear() {
this.cache.forEach((entry) => clearTimeout(entry.timeout))
this.cache.clear()
this.size = 0
return true
}
return entry
}
has(key) {
const entry = this.cache.get(key)
if (!entry) return false
MemoryCache.prototype.getValue = function (key) {
var entry = this.get(key)
if (Date.now() > entry.expire) {
this.delete(key)
return false
}
return entry && entry.value
}
MemoryCache.prototype.clear = function () {
Object.keys(this.cache).forEach(function (key) {
this.delete(key)
}, this)
return true
return true
}
}
module.exports = MemoryCache

View File

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

View File

@ -1,3 +1,4 @@
// 预先导入和绑定常用模块及函数
const encrypt = require('./crypto')
const CryptoJS = require('crypto-js')
const { default: axios } = require('axios')
@ -8,31 +9,50 @@ const tunnel = require('tunnel')
const fs = require('fs')
const path = require('path')
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(
path.resolve(tmpPath, './anonymous_token'),
'utf-8',
)
const { URLSearchParams, URL } = require('url')
const { APP_CONF } = require('../util/config.json')
const logger = require('./logger.js')
// 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 characters = 'abcdefghijklmnopqrstuvwxyz'
let randomString = ''
for (let i = 0; i < 6; i++)
randomString += characters.charAt(
Math.floor(Math.random() * characters.length),
)
return `${randomString}.${Date.now().toString()}.01.0`
for (let i = 0; i < 6; i++) {
randomString += characters.charAt(floor(random() * charactersLength))
}
return `${randomString}.${now().toString()}.01.0`
})()
// 预先定义osMap
const osMap = {
pc: {
os: 'pc',
appver: '3.0.18.203152',
osver: 'Microsoft-Windows-10-Professional-build-22631-64bit',
appver: '3.1.17.204416',
osver: 'Microsoft-Windows-10-Professional-build-19045-64bit',
channel: 'netease',
},
linux: {
@ -55,91 +75,128 @@ const osMap = {
},
}
const chooseUserAgent = (crypto, uaType = 'pc') => {
const userAgentMap = {
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',
},
linuxapi: {
linux:
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36',
},
api: {
pc: 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Safari/537.36 Chrome/91.0.4472.164 NeteaseMusicDesktop/3.0.18.203152',
android:
'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)',
},
}
return userAgentMap[crypto][uaType] || ''
// 预先定义userAgentMap
const userAgentMap = {
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',
},
linuxapi: {
linux:
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36',
},
api: {
pc: 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Safari/537.36 Chrome/91.0.4472.164 NeteaseMusicDesktop/3.0.18.203152',
android:
'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)',
},
}
// 预先定义常量
const DOMAIN = APP_CONF.domain
const API_DOMAIN = APP_CONF.apiDomain
const ENCRYPT_RESPONSE = APP_CONF.encryptResponse
const SPECIAL_STATUS_CODES = new Set([201, 302, 400, 502, 800, 801, 802, 803])
// chooseUserAgent函数
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,
__remember_me: 'true',
ntes_kaola_ad: '1',
_ntes_nuid: cookie._ntes_nuid || _ntes_nuid,
_ntes_nnid: cookie._ntes_nnid || `${_ntes_nuid},${now().toString()}`,
WNMCID: cookie.WNMCID || WNMCID,
WEVNSM: cookie.WEVNSM || '1.0.0',
osver: cookie.osver || os.osver,
deviceId: cookie.deviceId || global.deviceId,
os: cookie.os || os.os,
channel: cookie.channel || os.channel,
appver: cookie.appver || os.appver,
}
if (uri.indexOf('login') === -1) {
processedCookie['NMTID'] = CryptoJS.lib.WordArray.random(16).toString()
}
if (!processedCookie.MUSIC_U) {
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) => {
let headers = options.headers || {}
let ip = options.realIP || options.ip || ''
// logger.info(ip)
// 变量声明和初始化
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
}
// headers['X-Real-IP'] = '118.88.88.88'
let cookie = options.cookie || {}
if (typeof cookie === 'string') {
cookie = cookieToJson(cookie)
}
if (typeof cookie === 'object') {
let _ntes_nuid = CryptoJS.lib.WordArray.random(32).toString()
let os = osMap[cookie.os] || osMap['iphone']
cookie = {
...cookie,
__remember_me: 'true',
// NMTID: CryptoJS.lib.WordArray.random(16).toString(),
ntes_kaola_ad: '1',
_ntes_nuid: cookie._ntes_nuid || _ntes_nuid,
_ntes_nnid:
cookie._ntes_nnid || `${_ntes_nuid},${Date.now().toString()}`,
WNMCID: cookie.WNMCID || WNMCID,
WEVNSM: cookie.WEVNSM || '1.0.0',
osver: cookie.osver || os.osver,
deviceId: cookie.deviceId || global.deviceId,
os: cookie.os || os.os,
channel: cookie.channel || os.channel,
appver: cookie.appver || os.appver,
}
if (uri.indexOf('login') === -1) {
cookie['NMTID'] = CryptoJS.lib.WordArray.random(16).toString()
}
if (!cookie.MUSIC_U) {
// 游客
cookie.MUSIC_A = cookie.MUSIC_A || anonymous_token
}
if (typeof cookie === 'object') {
cookie = processCookieObject(cookie, uri)
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 (APP_CONF.encrypt) {
crypto = 'eapi'
} else {
crypto = 'api'
}
crypto = APP_CONF.encrypt ? 'eapi' : 'api'
}
// 根据加密方式加密请求数据目前任意uri都支持四种加密方式
const answer = { status: 500, body: {}, cookie: [] }
// 根据加密方式处理
switch (crypto) {
case 'weapi':
headers['Referer'] = APP_CONF.domain
headers['Referer'] = DOMAIN
headers['User-Agent'] = options.ua || chooseUserAgent('weapi')
data.csrf_token = csrfToken
encryptData = encrypt.weapi(data)
url = APP_CONF.domain + '/weapi/' + uri.substr(5)
url = DOMAIN + '/weapi/' + uri.substr(5)
break
case 'linuxapi':
@ -147,127 +204,127 @@ const createRequest = (uri, data, options) => {
options.ua || chooseUserAgent('linuxapi', 'linux')
encryptData = encrypt.linuxapi({
method: 'POST',
url: APP_CONF.domain + uri,
url: DOMAIN + uri,
params: data,
})
url = APP_CONF.domain + '/api/linux/forward'
url = DOMAIN + '/api/linux/forward'
break
case 'eapi':
case 'api':
// 两种加密方式都应生成客户端的cookie
// header创建
const header = {
osver: cookie.osver, //系统版本
osver: cookie.osver,
deviceId: cookie.deviceId,
os: cookie.os, //系统类型
appver: cookie.appver, // app版本
versioncode: cookie.versioncode || '140', //版本号
mobilename: cookie.mobilename || '', //设备model
buildver: cookie.buildver || Date.now().toString().substr(0, 10),
resolution: cookie.resolution || '1920x1080', //设备分辨率
os: cookie.os,
appver: cookie.appver,
versioncode: cookie.versioncode || '140',
mobilename: cookie.mobilename || '',
buildver: cookie.buildver || now().toString().substr(0, 10),
resolution: cookie.resolution || '1920x1080',
__csrf: csrfToken,
channel: cookie.channel, //下载渠道
requestId: `${Date.now()}_${Math.floor(Math.random() * 1000)
.toString()
.padStart(4, '0')}`,
channel: cookie.channel,
requestId: generateRequestId(),
...(options.checkToken
? { 'X-antiCheatToken': APP_CONF.checkToken }
: {}),
// clientSign: APP_CONF.clientSign,
}
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('; ')
headers['Cookie'] = createHeaderCookie(header)
headers['User-Agent'] = options.ua || chooseUserAgent('api', 'iphone')
if (crypto === 'eapi') {
// 使用eapi加密
data.header = header
data.e_r = toBoolean(
options.e_r !== undefined
? options.e_r
: data.e_r !== undefined
? data.e_r
: APP_CONF.encryptResponse,
) // 用于加密接口返回值
: ENCRYPT_RESPONSE,
)
encryptData = encrypt.eapi(uri, data)
url = APP_CONF.apiDomain + '/eapi/' + uri.substr(5)
url = API_DOMAIN + '/eapi/' + uri.substr(5)
} else if (crypto === 'api') {
// 不使用任何加密
url = APP_CONF.apiDomain + uri
url = API_DOMAIN + uri
encryptData = data
}
break
default:
// 未知的加密方式
logger.info('[ERR]', 'Unknown Crypto:', crypto)
console.log('[ERR]', 'Unknown Crypto:', crypto)
break
}
const answer = { status: 500, body: {}, cookie: [] }
// logger.info(headers, 'headers')
// console.log(url);
// settings创建
let settings = {
method: 'POST',
url: url,
headers: headers,
data: new URLSearchParams(encryptData).toString(),
httpAgent: new http.Agent({ keepAlive: true }),
httpsAgent: new https.Agent({ keepAlive: true }),
httpAgent: createHttpAgent(),
httpsAgent: createHttpsAgent(),
}
// e_r处理
if (data.e_r) {
settings = {
...settings,
encoding: null,
responseType: 'arraybuffer',
}
settings.encoding = null
settings.responseType = 'arraybuffer'
}
// 代理处理
if (options.proxy) {
if (options.proxy.indexOf('pac') > -1) {
settings.httpAgent = new PacProxyAgent(options.proxy)
settings.httpsAgent = new PacProxyAgent(options.proxy)
const agent = new PacProxyAgent(options.proxy)
settings.httpAgent = agent
settings.httpsAgent = agent
} else {
const purl = new URL(options.proxy)
if (purl.hostname) {
const agent = tunnel[
purl.protocol === 'https' ? 'httpsOverHttp' : 'httpOverHttp'
]({
proxy: {
host: purl.hostname,
port: purl.port || 80,
proxyAuth:
purl.username && purl.password
? purl.username + ':' + purl.password
: '',
},
})
settings.httpsAgent = agent
settings.httpAgent = agent
settings.proxy = false
} else {
console.error('代理配置无效,不使用代理')
try {
const purl = new URL(options.proxy)
if (purl.hostname) {
const isHttps = purl.protocol === 'https:'
const agent = tunnel[isHttps ? 'httpsOverHttp' : 'httpOverHttp']({
proxy: {
host: purl.hostname,
port: purl.port || 80,
proxyAuth:
purl.username && purl.password
? purl.username + ':' + purl.password
: '',
},
})
settings.httpsAgent = agent
settings.httpAgent = agent
settings.proxy = false
} else {
console.error('代理配置无效,不使用代理')
}
} catch (e) {
console.error('代理URL解析失败:', e.message)
}
}
} else {
settings.proxy = false
}
// console.log(settings.headers);
axios(settings)
.then((res) => {
const body = res.data
answer.cookie = (res.headers['set-cookie'] || []).map((x) =>
x.replace(/\s*Domain=[^(;|$)]+;*/, ''),
)
try {
if (crypto === 'eapi' && data.e_r) {
// eapi接口返回值被加密需要解密
answer.body = encrypt.eapiResDecrypt(
body.toString('hex').toUpperCase(),
)
} else {
answer.body =
typeof body == 'object' ? body : JSON.parse(body.toString())
typeof body === 'object' ? body : parse(body.toString())
}
if (answer.body.code) {
@ -275,28 +332,30 @@ const createRequest = (uri, data, options) => {
}
answer.status = Number(answer.body.code || res.status)
if (
[201, 302, 400, 502, 800, 801, 802, 803].indexOf(answer.body.code) >
-1
) {
// 特殊状态码
// 状态码检查使用Set提升查找性能
if (SPECIAL_STATUS_CODES.has(answer.body.code)) {
answer.status = 200
}
} catch (e) {
// logger.info(e)
// can't decrypt and can't parse directly
answer.body = body
answer.status = res.status
}
answer.status =
100 < answer.status && answer.status < 600 ? answer.status : 400
if (answer.status === 200) resolve(answer)
else reject(answer)
answer.status > 100 && answer.status < 600 ? answer.status : 400
if (answer.status === 200) {
resolve(answer)
} else {
console.log('[ERR]', answer)
reject(answer)
}
})
.catch((err) => {
answer.status = 502
answer.body = { code: 502, msg: err }
answer.body = { code: 502, msg: err.message || err }
console.log('[ERR]', answer)
reject(answer)
})
})