mirror of
https://github.com/NeteaseCloudMusicApiEnhanced/api-enhanced.git
synced 2026-10-24 00:21:23 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1bdc85998b | ||
|
|
49c7800afe |
@@ -14,15 +14,15 @@ jobs:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
platform: linux
|
||||
target: node18-linux-x64
|
||||
target: node22-linux-x64
|
||||
output: precompiled/app
|
||||
- os: windows-latest
|
||||
platform: win
|
||||
target: node18-win-x64
|
||||
target: node22-win-x64
|
||||
output: precompiled/app.exe
|
||||
- os: macos-latest
|
||||
platform: macos
|
||||
target: node18-macos-x64
|
||||
target: node22-macos-x64
|
||||
output: precompiled/app
|
||||
|
||||
steps:
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '18'
|
||||
node-version: '24'
|
||||
|
||||
- name: Setup pnpm
|
||||
run: npm install -g pnpm@9
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [18, 22, 24]
|
||||
node-version: [22, 24]
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
|
||||
@@ -78,17 +78,17 @@ jobs:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
platform: linux
|
||||
target: node18-linux-x64
|
||||
target: node22-linux-x64
|
||||
output: ncm-api-linux-x64
|
||||
|
||||
- os: windows-latest
|
||||
platform: windows
|
||||
target: node18-win-x64
|
||||
target: node22-win-x64
|
||||
output: ncm-api-win-x64.exe
|
||||
|
||||
- os: macos-latest
|
||||
platform: macos
|
||||
target: node18-macos-x64
|
||||
target: node22-macos-x64
|
||||
output: ncm-api-macos-x64
|
||||
|
||||
steps:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## 快速开始
|
||||
- **包管理器**:开发与 CI 使用 `pnpm`,`Dockerfile` 也用 pnpm(`pnpm@9 --frozen-lockfile`,匹配 `pnpm-lock.yaml`)。仓库没有 `yarn.lock`,不要引入 yarn。
|
||||
- **Node 版本**:README 推荐 Node 22+;`package.json` 的 `engines` 声明 `>=12`;CI/打包在 Node 18–24 上运行。现代 Node 均可。
|
||||
- **Node 版本**:`package.json` 的 `engines` 声明 `>=22`;CI/打包在 Node 22–24 上运行。现代 Node 均可。
|
||||
- **环境变量**:`server.js` 调用了 `dotenv.config()`,本地 `.env` 会被自动加载;所有支持的变量见 `.env.prod.example`。
|
||||
|
||||
## 常用命令
|
||||
@@ -11,7 +11,7 @@
|
||||
- 跑测试:`pnpm test`(Mocha,超时 60s)
|
||||
- Lint:`pnpm lint`;自动修复:`pnpm lint-fix`
|
||||
- 文档格式化检查/修复:`pnpm docs:check` / `pnpm docs:format`
|
||||
- 打包独立二进制:`pnpm pkgwin` / `pkglinux` / `pkgmacos`
|
||||
- 打包独立二进制:`pnpm pkgwin` / `pkglinux` / `pkgmacos`(@yao-pkg/pkg,node22 目标,`--no-bytecode` + `--public` 规避 node22 bytecode 回归)
|
||||
|
||||
## 目录内非路由文件
|
||||
- `plugins/`——内部上传辅助(`upload.js`、`songUpload.js`),被 `module/*` require,本身不是路由。
|
||||
|
||||
@@ -163,7 +163,7 @@ docker run -d -p 3000:3000 moefurina/ncm-api:latest
|
||||
```bash
|
||||
#!/bin/bash
|
||||
export PORT=9000
|
||||
/var/lang/node16/bin/node app.js
|
||||
/var/lang/node22/bin/node app.js
|
||||
```
|
||||
4. 完成部署后,访问 API 网关的 URL 即可
|
||||
|
||||
|
||||
@@ -3,6 +3,17 @@ const fs = require('fs')
|
||||
const path = require('path')
|
||||
const tmpPath = require('os').tmpdir()
|
||||
|
||||
// engines 字段只在 npm/pnpm 安装时生效,直接 node app.js 不会被拦截,
|
||||
// 这里给出友好的最低版本检查(>=22,与 package.json engines 保持一致)
|
||||
const [major] = process.versions.node.split('.').map(Number)
|
||||
if (major < 22) {
|
||||
console.error(
|
||||
`[${new Date().toLocaleString()}] Node.js >= 22 is required (current: ${process.versions.node}). ` +
|
||||
'请升级 Node.js 后重试 / Please upgrade Node.js to v22 or newer.',
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
async function start() {
|
||||
// 检测是否存在 anonymous_token 文件,没有则生成
|
||||
if (!fs.existsSync(path.resolve(tmpPath, 'anonymous_token'))) {
|
||||
|
||||
Vendored
-1
@@ -1,6 +1,5 @@
|
||||
export interface RequestBaseConfig {
|
||||
cookie?: string
|
||||
headers?: Record<string, string | string[]>
|
||||
realIP?: string // IPv4/IPv6 filled in X-Real-IP
|
||||
proxy?: string // HTTP proxy
|
||||
randomCNIP?: boolean // Whether to use a random Chinese IP address filled in X-Real-IP
|
||||
|
||||
@@ -25,7 +25,9 @@ moduleFiles.forEach((file) => {
|
||||
|
||||
obj[fn] = function (data = {}) {
|
||||
const cookie =
|
||||
typeof data.cookie === 'string' ? cookieToJson(data.cookie) : data.cookie
|
||||
typeof data.cookie === 'string'
|
||||
? cookieToJson(data.cookie)
|
||||
: data.cookie || {}
|
||||
|
||||
return fileModule(
|
||||
{
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
const QRCode = require('qrcode')
|
||||
const { generateChainId, getCookieHeader, hasCookie } = require('../util/index')
|
||||
const { generateChainId } = require('../util/index')
|
||||
|
||||
module.exports = (query) => {
|
||||
return new Promise(async (resolve) => {
|
||||
const platform = query.platform || 'pc'
|
||||
const headerCookie = getCookieHeader(query.headers)
|
||||
const cookie = hasCookie(query.cookie)
|
||||
? query.cookie
|
||||
: hasCookie(headerCookie)
|
||||
? headerCookie
|
||||
: ''
|
||||
const cookie = query.cookie || ''
|
||||
|
||||
// 构建基础URL
|
||||
let url = `https://music.163.com/login?codekey=${query.key}`
|
||||
|
||||
+2
-10
@@ -1,26 +1,18 @@
|
||||
// 听歌打卡
|
||||
|
||||
const createOption = require('../util/option.js')
|
||||
const { getCookieHeader, hasCookie } = require('../util/index')
|
||||
const { APP_CONF } = require('../util/config.json')
|
||||
const DOMAIN = APP_CONF.clDomian
|
||||
module.exports = async (query, request) => {
|
||||
// 注入 os=osx 的 cookie
|
||||
const headerCookie = getCookieHeader(query.headers)
|
||||
let cookie = hasCookie(query.cookie)
|
||||
? query.cookie
|
||||
: hasCookie(headerCookie)
|
||||
? headerCookie
|
||||
: process.env.NETEASE_COOKIE || ''
|
||||
let cookie = query.cookie || ''
|
||||
if (typeof cookie === 'object') {
|
||||
cookie = Object.assign({ os: 'osx' }, cookie)
|
||||
} else if (typeof cookie === 'string') {
|
||||
if (cookie.indexOf('os=') > -1) {
|
||||
cookie = cookie.replace(/os=[^;]+/g, 'os=osx')
|
||||
} else if (cookie) {
|
||||
cookie = cookie + '; os=osx'
|
||||
} else {
|
||||
cookie = 'os=osx'
|
||||
cookie = cookie + '; os=osx'
|
||||
}
|
||||
} else {
|
||||
cookie = 'os=osx'
|
||||
|
||||
@@ -13,7 +13,6 @@ const {
|
||||
buildMetaJson,
|
||||
doUpload,
|
||||
} = require('../util/ncbl')
|
||||
const { getCookieHeader, hasCookie } = require('../util/index')
|
||||
|
||||
module.exports = async (query, request) => {
|
||||
// --- 参数校验 ---
|
||||
@@ -33,12 +32,7 @@ module.exports = async (query, request) => {
|
||||
const sourceName = query.source || 'list'
|
||||
|
||||
// --- 解析认证上下文 ---
|
||||
const headerCookie = getCookieHeader(query.headers)
|
||||
const rawCookie = hasCookie(query.cookie)
|
||||
? query.cookie
|
||||
: hasCookie(headerCookie)
|
||||
? headerCookie
|
||||
: process.env.NETEASE_COOKIE || ''
|
||||
const rawCookie = query.cookie || ''
|
||||
const cookieObj = parseCookie(rawCookie)
|
||||
cookieObj.os = 'pc'
|
||||
const ctx = extractContext(cookieObj)
|
||||
|
||||
+6
-6
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@neteasecloudmusicapienhanced/api",
|
||||
"version": "4.41.1",
|
||||
"version": "5.0.0",
|
||||
"description": "全网最全的网易云音乐API接口 || A revival project for NeteaseCloudMusicApi Node.js Services (Half Refactor & Enhanced)",
|
||||
"scripts": {
|
||||
"dev": "nodemon app.js",
|
||||
@@ -11,9 +11,9 @@
|
||||
"prepare": "husky install",
|
||||
"docs:format": "node scripts/format-docs.js",
|
||||
"docs:check": "node scripts/format-docs.js --check",
|
||||
"pkgwin": "node scripts/build-pkg.js . -t node18-win-x64 -C GZip -o precompiled/app",
|
||||
"pkglinux": "node scripts/build-pkg.js . -t node18-linux-x64 -C GZip -o precompiled/app",
|
||||
"pkgmacos": "node scripts/build-pkg.js . -t node18-macos-x64 -C GZip -o precompiled/app"
|
||||
"pkgwin": "node scripts/build-pkg.js . -t node22-win-x64 -C GZip -o precompiled/app --no-bytecode --public-packages \"*\" --public",
|
||||
"pkglinux": "node scripts/build-pkg.js . -t node22-linux-x64 -C GZip -o precompiled/app --no-bytecode --public-packages \"*\" --public",
|
||||
"pkgmacos": "node scripts/build-pkg.js . -t node22-macos-x64 -C GZip -o precompiled/app --no-bytecode --public-packages \"*\" --public"
|
||||
},
|
||||
"bin": "./app.js",
|
||||
"pkg": {
|
||||
@@ -52,7 +52,7 @@
|
||||
},
|
||||
"homepage": "https://neteasecloudmusicapienhanced.js.org/",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=22"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.js": [
|
||||
@@ -112,7 +112,7 @@
|
||||
"lint-staged": "^16.4.0",
|
||||
"mocha": "^11.8.0",
|
||||
"nodemon": "^3.1.14",
|
||||
"pkg": "^5.8.1",
|
||||
"@yao-pkg/pkg": "^6.23.0",
|
||||
"power-assert": "^1.6.1",
|
||||
"prettier": "^3.9.9",
|
||||
"typescript": "^5.9.3"
|
||||
|
||||
Generated
+642
-262
File diff suppressed because it is too large
Load Diff
+16
-8
@@ -1,10 +1,10 @@
|
||||
/**
|
||||
* Invoke the pkg packager (cross-platform, independent of node_modules layout).
|
||||
*
|
||||
* Background: on Windows, pnpm links node_modules/pkg to
|
||||
* node_modules/.pnpm/pkg@<ver>/node_modules/pkg via a junction. Some CI
|
||||
* environments (pnpm 11 + Windows runner) may fail to create that link,
|
||||
* so executing node_modules/pkg/lib-es5/bin.js directly throws
|
||||
* Background: on Windows, pnpm links node_modules/@yao-pkg/pkg to
|
||||
* node_modules/.pnpm/@yao-pkg+pkg@<ver>/node_modules/@yao-pkg/pkg via a junction.
|
||||
* Some CI environments (pnpm 11 + Windows runner) may fail to create that link,
|
||||
* so executing node_modules/@yao-pkg/pkg/lib-es5/bin.js directly throws
|
||||
* MODULE_NOT_FOUND.
|
||||
*
|
||||
* This script first resolves pkg via require.resolve and falls back to
|
||||
@@ -17,16 +17,24 @@ const path = require('path')
|
||||
|
||||
function resolvePkgBin() {
|
||||
try {
|
||||
return require.resolve('pkg/lib-es5/bin.js')
|
||||
return require.resolve('@yao-pkg/pkg/lib-es5/bin.js')
|
||||
} catch (err) {
|
||||
// require.resolve failed (e.g. missing node_modules/pkg link): scan .pnpm
|
||||
// require.resolve failed (e.g. missing node_modules/@yao-pkg/pkg link): scan .pnpm
|
||||
const pnpmDir = path.join(process.cwd(), 'node_modules', '.pnpm')
|
||||
if (fs.existsSync(pnpmDir)) {
|
||||
const candidates = fs
|
||||
.readdirSync(pnpmDir)
|
||||
.filter((dir) => dir.startsWith('pkg@'))
|
||||
.filter((dir) => dir.startsWith('@yao-pkg+pkg@'))
|
||||
.map((dir) =>
|
||||
path.join(pnpmDir, dir, 'node_modules', 'pkg', 'lib-es5', 'bin.js'),
|
||||
path.join(
|
||||
pnpmDir,
|
||||
dir,
|
||||
'node_modules',
|
||||
'@yao-pkg',
|
||||
'pkg',
|
||||
'lib-es5',
|
||||
'bin.js',
|
||||
),
|
||||
)
|
||||
.filter((file) => fs.existsSync(file))
|
||||
if (candidates.length) {
|
||||
|
||||
@@ -6,7 +6,7 @@ const request = require('./util/request')
|
||||
const packageJSON = require('./package.json')
|
||||
const exec = require('child_process').exec
|
||||
const cache = require('./util/apicache').middleware
|
||||
const { cookieToJson, getCookieHeader, hasCookie } = require('./util/index')
|
||||
const { cookieToJson } = require('./util/index')
|
||||
const fileUpload = require('express-fileupload')
|
||||
const decode = require('safe-decode-uri-component')
|
||||
const logger = require('./util/logger.js')
|
||||
@@ -225,15 +225,14 @@ async function constructServer(moduleDefs) {
|
||||
*/
|
||||
app.use((req, _, next) => {
|
||||
req.cookies = {}
|
||||
getCookieHeader(req.headers)
|
||||
.split(';')
|
||||
.forEach((pair) => {
|
||||
let crack = pair.indexOf('=')
|
||||
if (crack < 1 || crack == pair.length - 1) return
|
||||
req.cookies[decode(pair.slice(0, crack)).trim()] = decode(
|
||||
pair.slice(crack + 1),
|
||||
).trim()
|
||||
})
|
||||
//;(req.headers.cookie || '').split(/\s*;\s*/).forEach((pair) => { // Polynomial regular expression //
|
||||
;(req.headers.cookie || '').split(/;\s+|(?<!\s)\s+$/g).forEach((pair) => {
|
||||
let crack = pair.indexOf('=')
|
||||
if (crack < 1 || crack == pair.length - 1) return
|
||||
req.cookies[decode(pair.slice(0, crack)).trim()] = decode(
|
||||
pair.slice(crack + 1),
|
||||
).trim()
|
||||
})
|
||||
next()
|
||||
})
|
||||
|
||||
@@ -292,19 +291,13 @@ async function constructServer(moduleDefs) {
|
||||
}
|
||||
})
|
||||
|
||||
const queryCookie = req.query && req.query.cookie
|
||||
const bodyCookie = req.body && req.body.cookie
|
||||
const fileCookie = req.files && req.files.cookie
|
||||
const cookie = hasCookie(fileCookie)
|
||||
? fileCookie
|
||||
: hasCookie(bodyCookie)
|
||||
? bodyCookie
|
||||
: hasCookie(queryCookie)
|
||||
? queryCookie
|
||||
: req.cookies
|
||||
const query = Object.assign({}, req.query, req.body, req.files, {
|
||||
cookie,
|
||||
})
|
||||
let query = Object.assign(
|
||||
{},
|
||||
{ cookie: req.cookies },
|
||||
req.query,
|
||||
req.body,
|
||||
req.files,
|
||||
)
|
||||
|
||||
try {
|
||||
let usedCrypto = ''
|
||||
|
||||
+4
-33
@@ -94,42 +94,15 @@ module.exports = {
|
||||
// 优化:使用for循环替代forEach,性能更好
|
||||
for (let i = 0, len = cookieArr.length; i < len; i++) {
|
||||
let item = cookieArr[i]
|
||||
let separator = item.indexOf('=')
|
||||
// 只按第一个等号拆分,保留 Cookie 值中的等号
|
||||
if (separator > 0) {
|
||||
obj[item.slice(0, separator).trim()] = item.slice(separator + 1).trim()
|
||||
let arr = item.split('=')
|
||||
// 优化:使用严格等于
|
||||
if (arr.length === 2) {
|
||||
obj[arr[0].trim()] = arr[1].trim()
|
||||
}
|
||||
}
|
||||
return obj
|
||||
},
|
||||
|
||||
hasCookie(cookie) {
|
||||
if (typeof cookie === 'string') return cookie.length > 0
|
||||
return (
|
||||
cookie !== null &&
|
||||
typeof cookie === 'object' &&
|
||||
Object.keys(cookie).length > 0
|
||||
)
|
||||
},
|
||||
|
||||
getCookieHeader(headers) {
|
||||
if (!headers || typeof headers !== 'object') return ''
|
||||
|
||||
if (typeof headers.get === 'function') {
|
||||
const cookie = headers.get('cookie')
|
||||
if (typeof cookie === 'string') return cookie
|
||||
}
|
||||
|
||||
const headerName = Object.keys(headers).find(
|
||||
(name) => name.toLowerCase() === 'cookie',
|
||||
)
|
||||
if (!headerName) return ''
|
||||
|
||||
const cookie = headers[headerName]
|
||||
if (Array.isArray(cookie)) return cookie.join('; ')
|
||||
return typeof cookie === 'string' ? cookie : ''
|
||||
},
|
||||
|
||||
cookieObjToString(cookie) {
|
||||
// 优化:使用预绑定的keys函数和for循环
|
||||
const cookieKeys = keys(cookie)
|
||||
@@ -253,8 +226,6 @@ const cookieToJsonOptimized = (function() {
|
||||
function getCookieValue(cookieStr, name) {
|
||||
if (!cookieStr) return ''
|
||||
|
||||
if (typeof cookieStr === 'object') return cookieStr[name] || ''
|
||||
|
||||
const cookies = '; ' + cookieStr
|
||||
const parts = cookies.split('; ' + name + '=')
|
||||
if (parts.length === 2) return parts.pop().split(';').shift()
|
||||
|
||||
+1
-9
@@ -1,15 +1,7 @@
|
||||
const { getCookieHeader, hasCookie } = require('./index')
|
||||
|
||||
const createOption = (query, crypto = '', checkToken = false) => {
|
||||
const headerCookie = getCookieHeader(query.headers)
|
||||
|
||||
return {
|
||||
crypto: query.crypto || crypto || '',
|
||||
cookie: hasCookie(query.cookie)
|
||||
? query.cookie
|
||||
: hasCookie(headerCookie)
|
||||
? headerCookie
|
||||
: process.env.NETEASE_COOKIE,
|
||||
cookie: query.cookie || process.env.NETEASE_COOKIE,
|
||||
ua: query.ua || '',
|
||||
proxy: query.proxy,
|
||||
realIP: query.realIP,
|
||||
|
||||
+1
-9
@@ -13,8 +13,6 @@ const tmpPath = require('os').tmpdir()
|
||||
const {
|
||||
cookieToJson,
|
||||
cookieObjToString,
|
||||
getCookieHeader,
|
||||
hasCookie,
|
||||
toBoolean,
|
||||
generateRandomChineseIP,
|
||||
} = require('./index')
|
||||
@@ -245,19 +243,13 @@ const createRequest = async (uri, data, options) => {
|
||||
headers['X-Forwarded-For'] = ip
|
||||
}
|
||||
|
||||
let cookie = hasCookie(options.cookie)
|
||||
? options.cookie
|
||||
: getCookieHeader(options.headers)
|
||||
if (!hasCookie(cookie)) cookie = {}
|
||||
let cookie = options.cookie || {}
|
||||
if (typeof cookie === 'string') {
|
||||
cookie = cookieToJson(cookie)
|
||||
}
|
||||
|
||||
if (typeof cookie === 'object') {
|
||||
cookie = processCookieObject(cookie, crypto)
|
||||
Object.keys(headers).forEach((name) => {
|
||||
if (name.toLowerCase() === 'cookie') delete headers[name]
|
||||
})
|
||||
headers['Cookie'] = cookieObjToString(cookie)
|
||||
}
|
||||
let url = ''
|
||||
|
||||
Reference in New Issue
Block a user