Compare commits

...

21 Commits

Author SHA1 Message Date
1454ee216c
fix: 请求异常 (83ee2cda34a20f64c6d343f7dfe22aa6a1c84b3d) 2026-06-20 23:28:08 +08:00
83ee2cda34
feat: add NCBL encrypted scrobble interface and update related documentation
- Implemented new scrobble endpoint for NCBL encrypted logs in `scrobble_v1.js`.
- Updated `scrobble.js` to use domain from config.
- Enhanced `scrobble.html` with tabbed interface for original and NCBL versions, including new input fields.
- Added cookie handling and quick fill functionality for both versions.
- Updated `config.json` to include new domain configurations.
- Introduced `ncbl.js` for NCBL encryption utilities.
- Improved documentation in `home.md` to reflect changes in API usage.
2026-06-20 22:51:51 +08:00
3c352b5752
feat: 新增听歌打卡示例页面 2026-06-20 21:16:14 +08:00
60ee927e32
fix: 修复听歌打卡接口 (#167) 2026-06-19 15:11:31 +08:00
eb07a525eb
feat: 支持显示加密方式 2026-06-18 23:46:11 +08:00
6fc4231142
feat(docs-format): 增加文档格式化工具 2026-06-18 23:05:44 +08:00
fa69736300
revert: some changes
This reverts commit c9598664367ca826b7503e6b4875ec222ad20633.
2026-06-18 22:52:35 +08:00
73a5e9d788
Revert "feat: 增加文档格式化工具"
This reverts commit 6550c7c827e18479a2e01865d6067cb5dbea5dd2.
2026-06-18 22:51:34 +08:00
dc6f56e8a8
Revert "fix(deploy): 修复部分启动失败 (#196)"
This reverts commit 395a80e74b9e69b8bfadfc19d77d57121f68087d.
2026-06-18 22:50:53 +08:00
2c9da8507d
Revert "refactor: 优化serverless入口文件"
This reverts commit 1db354464fa20bed8dc70323c066fe12a2a02348.
2026-06-18 22:50:51 +08:00
1db354464f
refactor: 优化serverless入口文件 2026-06-18 22:29:24 +08:00
395a80e74b
fix(deploy): 修复部分启动失败 (#196) 2026-06-18 21:00:06 +08:00
6550c7c827
feat: 增加文档格式化工具 2026-06-13 23:49:30 +08:00
c959866436
feat: 增强配置生成逻辑,添加重试机制和日志记录 2026-06-13 21:21:33 +08:00
e3a1c041b6
fix: 修复游客登陆接口 2026-06-09 00:09:48 +08:00
e2324e26da
Merge remote-tracking branch 'origin/dependabot/github_actions/docker/login-action-4' 2026-06-09 00:09:09 +08:00
f01e770c35
Merge remote-tracking branch 'origin/dependabot/npm_and_yarn/types/node-25.9.1' 2026-06-09 00:08:41 +08:00
abb56ec014
Merge remote-tracking branch 'origin/dependabot/github_actions/aormsby/Fork-Sync-With-Upstream-action-3.4.3' 2026-06-09 00:08:23 +08:00
dependabot[bot]
0300c1b51e
build(packages): bump @types/node from 25.5.0 to 25.9.2
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 25.5.0 to 25.9.2.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 25.9.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-06 07:42:44 +00:00
dependabot[bot]
a9c33de182
chore(deps): bump docker/login-action from 3 to 4
Bumps [docker/login-action](https://github.com/docker/login-action) from 3 to 4.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-30 10:24:02 +00:00
dependabot[bot]
6175d55f89
chore(deps): bump aormsby/Fork-Sync-With-Upstream-action
Bumps [aormsby/Fork-Sync-With-Upstream-action](https://github.com/aormsby/fork-sync-with-upstream-action) from 3.4.2 to 3.4.3.
- [Release notes](https://github.com/aormsby/fork-sync-with-upstream-action/releases)
- [Commits](https://github.com/aormsby/fork-sync-with-upstream-action/compare/v3.4.2...v3.4.3)

---
updated-dependencies:
- dependency-name: aormsby/Fork-Sync-With-Upstream-action
  dependency-version: 3.4.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-01 14:54:43 +00:00
21 changed files with 2028 additions and 213 deletions

View File

@ -260,13 +260,13 @@ jobs:
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: Log in to Docker Hub - name: Log in to Docker Hub
uses: docker/login-action@v3 uses: docker/login-action@v4
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Log in to GitHub Container Registry - name: Log in to GitHub Container Registry
uses: docker/login-action@v3 uses: docker/login-action@v4
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}

View File

@ -22,7 +22,7 @@ jobs:
# Step 2: run the sync action # Step 2: run the sync action
- name: Sync upstream changes - name: Sync upstream changes
id: sync id: sync
uses: aormsby/Fork-Sync-With-Upstream-action@v3.4.2 uses: aormsby/Fork-Sync-With-Upstream-action@v3.4.3
with: with:
upstream_sync_repo: NeteaseCloudMusicApiEnhanced/api-enhanced upstream_sync_repo: NeteaseCloudMusicApiEnhanced/api-enhanced
upstream_sync_branch: main upstream_sync_branch: main

23
AGENTS.md Normal file
View File

@ -0,0 +1,23 @@
# Agent Instructions for NeteaseCloudMusicApiEnhanced
## Quick Start
- **Package Manager**: Use `pnpm` (not npm or yarn).
- **Node Version**: Requires Node.js 18 or later.
## Developer Commands
- **Install dependencies**: `pnpm i`
- **Start server**: `pnpm start` or `node app.js`
- **Start dev server**: `pnpm dev` (uses nodemon)
- **Run tests**: `pnpm test` (uses Mocha)
- **Linting**: `pnpm lint` (check) or `pnpm lint-fix` (auto-fix)
## Architecture & Entrypoints
- **Executable Server**: `app.js` is the main entrypoint for running the API server.
- **Module Exports**: `main.js` is the entrypoint when the project is imported as a Node.js dependency.
- **API Endpoints**: Located in the `module/` directory. Each file typically corresponds to an API route.
- **Core Utilities**: Request handling, encryption, and core utilities are found in the `util/` directory.
## Important Gotchas & Quirks
- **Environment Variables**: The server defaults to port 3000 but can be overridden with the `PORT` environment variable.
- **Proxy Variables**: Be very careful with proxy environment variables (`http_proxy`, `https_proxy`, `no_proxy`). The request library (like axios) will automatically pick these up. If they point to an unavailable proxy (especially common in Docker environments), requests will fail silently or throw connection errors.
- **Code Style**: The project uses ESLint and Prettier. Always run `pnpm lint-fix` before committing changes to ensure formatting consistency.

View File

@ -225,6 +225,8 @@ pnpm test
[chaunsin/netease-cloud-music](https://github.com/chaunsin/netease-cloud-music) [chaunsin/netease-cloud-music](https://github.com/chaunsin/netease-cloud-music)
[folltoshe/netease-report-listen-song](https://github.com/folltoshe/netease-report-listen-song)
### SDK 生态 ### SDK 生态
| 语言 | 作者 | 地址 | 类型 | | 语言 | 作者 | 地址 | 类型 |

View File

@ -1,3 +1,5 @@
// 获取游客cookie
const CryptoJS = require('crypto-js') const CryptoJS = require('crypto-js')
const path = require('path') const path = require('path')
const fs = require('fs') const fs = require('fs')
@ -37,7 +39,7 @@ module.exports = async (query, request) => {
let result = await request( let result = await request(
`/api/register/anonimous`, `/api/register/anonimous`,
data, data,
createOption(query, 'weapi'), createOption(query, 'xeapi'),
) )
if (result.body.code === 200) { if (result.body.code === 200) {
result = { result = {

View File

@ -1,8 +1,42 @@
// 听歌打卡 // 听歌打卡
const createOption = require('../util/option.js') const createOption = require('../util/option.js')
module.exports = (query, request) => { const { APP_CONF } = require('../util/config.json')
const data = { const DOMAIN = APP_CONF.clDomian
module.exports = async (query, request) => {
// 注入 os=osx 的 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 {
cookie = cookie + '; os=osx'
}
} else {
cookie = 'os=osx'
}
query.cookie = cookie
// 1) startplay → 进「最近播放」
const startplayData = {
logs: JSON.stringify([
{
action: 'startplay',
json: {
id: query.id,
type: 'song',
mainsite: '1',
mainsiteWeb: '1',
content: `id=${query.sourceid}`,
},
},
]),
}
// 2) play → 涨「听歌排行」计数
const playData = {
logs: JSON.stringify([ logs: JSON.stringify([
{ {
action: 'play', action: 'play',
@ -15,12 +49,30 @@ module.exports = (query, request) => {
type: 'song', type: 'song',
wifi: 0, wifi: 0,
source: 'list', source: 'list',
mainsite: 1, mainsite: '1',
content: '', mainsiteWeb: '1',
content: `id=${query.sourceid}`,
}, },
}, },
]), ]),
} }
return request(`/api/feedback/weblog`, data, createOption(query, 'weapi')) const option = createOption(query, 'eapi')
option.domain = DOMAIN
// 发送两次请求
const res1 = await request(`/api/feedback/weblog`, startplayData, option)
const res2 = await request(`/api/feedback/weblog`, playData, option)
return {
status: 200,
body: {
code: 200,
data: 'success',
details: {
startplay: res1.body,
play: res2.body,
},
},
}
} }

128
module/scrobble_v1.js Normal file
View File

@ -0,0 +1,128 @@
// 听歌打卡 - NCBL 加密版 (仿桌面客户端 PLV/PLD 上报)
// 复制自 https://github.com/folltoshe/netease-report-listen-song 的 PC 端日志上报方式
//
// PLV 和 PLD 分两次独立上传
const {
buildPlv,
buildPld,
buildRecords,
extractContext,
parseCookie,
buildCookieStr,
buildMetaJson,
doUpload,
} = require('../util/ncbl')
module.exports = async (query, request) => {
// --- 参数校验 ---
const songId = Number(query.id)
if (!songId || isNaN(songId)) {
return { status: 400, body: { code: 400, msg: '缺少有效的 id (歌曲ID)' } }
}
const playTime = Number(query.time)
if (isNaN(playTime) || playTime <= 0) {
return {
status: 400,
body: { code: 400, msg: '缺少有效的 time (播放时长)' },
}
}
const totalTime = Number(query.total) || playTime
const sourceId = String(query.sourceid || query.sourceId || '')
const sourceName = query.source || 'list'
// --- 解析认证上下文 ---
const rawCookie = query.cookie || ''
const cookieObj = parseCookie(rawCookie)
cookieObj.os = 'pc'
const ctx = extractContext(cookieObj)
// 兜底取 token
if (!ctx.auth.token && rawCookie) {
const parsed =
typeof rawCookie === 'string' ? parseCookie(rawCookie) : rawCookie
ctx.auth.token = parsed.MUSIC_U || ''
}
if (!ctx.auth.token) {
return { status: 401, body: { code: 401, msg: '缺少 MUSIC_U 鉴权令牌' } }
}
// --- 构建歌曲和来源 ---
const song = {
id: songId,
name: query.name || '',
artist: query.artist || '',
bitrate: Number(query.bitrate) || 320,
level: query.level || 'exhigh',
vip: query.vip === 'true' || query.vip === true,
time: totalTime,
}
const source = {
id: sourceId || String(songId),
type: 'track',
name: sourceName,
}
const metaJson = buildMetaJson(ctx)
const cookieStr = buildCookieStr(ctx)
const ts = Math.floor(Date.now() / 1000)
const played = Math.min(playTime, totalTime)
const plvBody = buildRecords([
{ time: ts, action: '_plv', data: buildPlv(ctx, song, source) },
])
const pldBody = buildRecords([
{ time: ts, action: '_pld', data: buildPld(ctx, song, source, played) },
])
try {
// 1) 上传 PLV
const plv = await doUpload(ctx, metaJson, plvBody, cookieStr, 'PLV')
if (!plv.success) {
const rateMsg =
plv.respBody?.data?.rate != null
? ` (rate=${plv.respBody.data.rate})`
: ''
return {
status: 200,
body: {
code: plv.respBody?.code || -1,
msg: `PLV 上报失败${rateMsg}`,
details: plv.respBody,
},
}
}
// 2) 上传 PLD
const pld = await doUpload(ctx, metaJson, pldBody, cookieStr, 'PLD')
if (!pld.success) {
return {
status: 200,
body: {
code: pld.respBody?.code || -1,
msg: 'PLV 成功但 PLD 失败',
details: { plv: plv.respBody, pld: pld.respBody },
},
}
}
return {
status: 200,
body: {
code: 200,
data: 'scrobble_v1 上报成功',
details: {
plv: { fileName: plv.fileName, payloadSize: plv.payload.length },
pld: { fileName: pld.fileName, payloadSize: pld.payload.length },
},
},
}
} catch (err) {
return {
status: 502,
body: { code: 502, msg: `请求异常: ${err.message || err}` },
}
}
}

View File

@ -1,6 +1,6 @@
{ {
"name": "@neteasecloudmusicapienhanced/api", "name": "@neteasecloudmusicapienhanced/api",
"version": "4.35.0", "version": "4.36.1",
"description": "全网最全的网易云音乐API接口 || A revival project for NeteaseCloudMusicApi Node.js Services (Half Refactor & Enhanced) || 网易云音乐 API 备份 + 增强 || 本项目自原版v4.28.0版本后开始自行维护", "description": "全网最全的网易云音乐API接口 || A revival project for NeteaseCloudMusicApi Node.js Services (Half Refactor & Enhanced) || 网易云音乐 API 备份 + 增强 || 本项目自原版v4.28.0版本后开始自行维护",
"scripts": { "scripts": {
"dev": "nodemon app.js", "dev": "nodemon app.js",
@ -9,6 +9,8 @@
"lint": "eslint \"**/*.{js,ts}\"", "lint": "eslint \"**/*.{js,ts}\"",
"lint-fix": "eslint --fix \"**/*.{js,ts}\"", "lint-fix": "eslint --fix \"**/*.{js,ts}\"",
"prepare": "husky install", "prepare": "husky install",
"docs:format": "node scripts/format-docs.js",
"docs:check": "node scripts/format-docs.js --check",
"pkgwin": "pkg . -t node18-win-x64 -C GZip -o precompiled/app", "pkgwin": "pkg . -t node18-win-x64 -C GZip -o precompiled/app",
"pkglinux": "pkg . -t node18-linux-x64 -C GZip -o precompiled/app", "pkglinux": "pkg . -t node18-linux-x64 -C GZip -o precompiled/app",
"pkgmacos": "pkg . -t node18-macos-x64 -C GZip -o precompiled/app" "pkgmacos": "pkg . -t node18-macos-x64 -C GZip -o precompiled/app"
@ -73,14 +75,14 @@
"data" "data"
], ],
"dependencies": { "dependencies": {
"@neteasecloudmusicapienhanced/unblockmusic-utils": "^0.3.2", "@neteasecloudmusicapienhanced/unblockmusic-utils": "^0.3.3",
"axios": "^1.17.0", "axios": "^1.18.0",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"dotenv": "^17.4.2", "dotenv": "^17.4.2",
"express": "^5.2.1", "express": "^5.2.1",
"express-fileupload": "^1.5.2", "express-fileupload": "^1.5.2",
"gzip": "^0.1.0", "gzip": "^0.1.0",
"music-metadata": "^11.12.3", "music-metadata": "^11.13.0",
"node-forge": "^1.4.0", "node-forge": "^1.4.0",
"pac-proxy-agent": "^7.2.0", "pac-proxy-agent": "^7.2.0",
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
@ -95,9 +97,9 @@
"@types/express": "^5.0.6", "@types/express": "^5.0.6",
"@types/express-fileupload": "^1.5.1", "@types/express-fileupload": "^1.5.1",
"@types/mocha": "^10.0.10", "@types/mocha": "^10.0.10",
"@types/node": "25.5.0", "@types/node": "25.9.2",
"@typescript-eslint/eslint-plugin": "^8.60.1", "@typescript-eslint/eslint-plugin": "^8.61.1",
"@typescript-eslint/parser": "^8.60.1", "@typescript-eslint/parser": "^8.61.1",
"eslint": "^9.39.4", "eslint": "^9.39.4",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"eslint-plugin-html": "^8.1.4", "eslint-plugin-html": "^8.1.4",
@ -110,7 +112,7 @@
"nodemon": "^3.1.14", "nodemon": "^3.1.14",
"pkg": "^5.8.1", "pkg": "^5.8.1",
"power-assert": "^1.6.1", "power-assert": "^1.6.1",
"prettier": "^3.8.3", "prettier": "^3.8.4",
"typescript": "^5.9.3" "typescript": "^5.9.3"
}, },
"publishConfig": { "publishConfig": {

326
pnpm-lock.yaml generated
View File

@ -9,11 +9,11 @@ importers:
.: .:
dependencies: dependencies:
'@neteasecloudmusicapienhanced/unblockmusic-utils': '@neteasecloudmusicapienhanced/unblockmusic-utils':
specifier: ^0.3.2 specifier: ^0.3.3
version: 0.3.2 version: 0.3.3
axios: axios:
specifier: ^1.17.0 specifier: ^1.18.0
version: 1.17.0 version: 1.18.0
crypto-js: crypto-js:
specifier: ^4.2.0 specifier: ^4.2.0
version: 4.2.0 version: 4.2.0
@ -30,8 +30,8 @@ importers:
specifier: ^0.1.0 specifier: ^0.1.0
version: 0.1.0 version: 0.1.0
music-metadata: music-metadata:
specifier: ^11.12.3 specifier: ^11.13.0
version: 11.12.3 version: 11.13.0
node-forge: node-forge:
specifier: ^1.4.0 specifier: ^1.4.0
version: 1.4.0 version: 1.4.0
@ -70,14 +70,14 @@ importers:
specifier: ^10.0.10 specifier: ^10.0.10
version: 10.0.10 version: 10.0.10
'@types/node': '@types/node':
specifier: 25.5.0 specifier: 25.9.2
version: 25.5.0 version: 25.9.2
'@typescript-eslint/eslint-plugin': '@typescript-eslint/eslint-plugin':
specifier: ^8.60.1 specifier: ^8.61.1
version: 8.60.1(@typescript-eslint/parser@8.60.1(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3) version: 8.61.1(@typescript-eslint/parser@8.61.1(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3)
'@typescript-eslint/parser': '@typescript-eslint/parser':
specifier: ^8.60.1 specifier: ^8.61.1
version: 8.60.1(eslint@9.39.4)(typescript@5.9.3) version: 8.61.1(eslint@9.39.4)(typescript@5.9.3)
eslint: eslint:
specifier: ^9.39.4 specifier: ^9.39.4
version: 9.39.4 version: 9.39.4
@ -89,7 +89,7 @@ importers:
version: 8.1.4 version: 8.1.4
eslint-plugin-prettier: eslint-plugin-prettier:
specifier: ^5.5.6 specifier: ^5.5.6
version: 5.5.6(eslint-config-prettier@10.1.8(eslint@9.39.4))(eslint@9.39.4)(prettier@3.8.3) version: 5.5.6(eslint-config-prettier@10.1.8(eslint@9.39.4))(eslint@9.39.4)(prettier@3.8.4)
globals: globals:
specifier: ^17.6.0 specifier: ^17.6.0
version: 17.6.0 version: 17.6.0
@ -115,8 +115,8 @@ importers:
specifier: ^1.6.1 specifier: ^1.6.1
version: 1.6.1 version: 1.6.1
prettier: prettier:
specifier: ^3.8.3 specifier: ^3.8.4
version: 3.8.3 version: 3.8.4
typescript: typescript:
specifier: ^5.9.3 specifier: ^5.9.3
version: 5.9.3 version: 5.9.3
@ -222,8 +222,8 @@ packages:
'@jridgewell/trace-mapping@0.3.31': '@jridgewell/trace-mapping@0.3.31':
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
'@neteasecloudmusicapienhanced/unblockmusic-utils@0.3.2': '@neteasecloudmusicapienhanced/unblockmusic-utils@0.3.3':
resolution: {integrity: sha512-H1ckEDXxR+sLUZKzCPhlP8kfSKgEZZp8GSL4+2BZcmCyerwCbSZzMxX9doacHfFjThRjmq90DbsrRnhnKAvrzA==} resolution: {integrity: sha512-Lm2Zxod4Qfhp3HNDmaHxzJPLiMfMNkUT2PXp0VVrDW+/dDZBfSANO+pnCHx+MkK7tDUb3wGwYp5q2w9Kw1+4OA==}
hasBin: true hasBin: true
'@nodelib/fs.scandir@2.1.5': '@nodelib/fs.scandir@2.1.5':
@ -286,8 +286,8 @@ packages:
'@types/mocha@10.0.10': '@types/mocha@10.0.10':
resolution: {integrity: sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==} resolution: {integrity: sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==}
'@types/node@25.5.0': '@types/node@25.9.2':
resolution: {integrity: sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==} resolution: {integrity: sha512-G05zqtJhcDLb8uslf5EjCxXg9G1KQxiV8OS0R26IC//Eoyitzqe8z37I7cqvnZlrlSfgocQRfSn/AHBZJJFyGw==}
'@types/qs@6.15.1': '@types/qs@6.15.1':
resolution: {integrity: sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==} resolution: {integrity: sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==}
@ -301,63 +301,63 @@ packages:
'@types/serve-static@2.2.0': '@types/serve-static@2.2.0':
resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==} resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==}
'@typescript-eslint/eslint-plugin@8.60.1': '@typescript-eslint/eslint-plugin@8.61.1':
resolution: {integrity: sha512-JQ4S5GB0tfjO8BuJ4fcX+HodkzJjYBV+7OJ+wLygaX7OGQ7FudyHL4NSCA6ob+w3Yn+5MkKIozOwQhXeM7opVg==} resolution: {integrity: sha512-ZPlVl3PB3et/59Ne0fv/sci6ZXz4T4Hp4nTJ56i/Y0gR89ARb+KphojTq6j+56E5PIezmOIOOWyY+aWQFd+IkQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies: peerDependencies:
'@typescript-eslint/parser': ^8.60.1 '@typescript-eslint/parser': ^8.61.1
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0' typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/parser@8.60.1': '@typescript-eslint/parser@8.61.1':
resolution: {integrity: sha512-A0M6ua6H252bVjPvvtSgl2QA4+ET9S5Mtkb2GDyTxIhH/C4qDItT7RQNO5PhMC6NXGYXOR9dIalcDDgBKT7oFA==} resolution: {integrity: sha512-PJ5vePq5/ognBbrIcoC5+SHO5dfpeLPzP9FpLkzWrguoYQEeeSjlJpVwOpo1JRSTEi7dRcwNy4h4dzV70PqHcg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies: peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0' typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/project-service@8.60.1': '@typescript-eslint/project-service@8.61.1':
resolution: {integrity: sha512-eXkTH2bxmXlqD1RnOPmLZ9ZM9D3VwSx04JOwBnP9RQ+yUA5a2Mu7SfW8uaV2Aon53NJzZlZYuX7tn91Izf+xaw==} resolution: {integrity: sha512-PrC4JYGmR241lYnfhmKGTXkFqv8+ymbTFgSAY0fVXpY82/QkMw5TZPl+vGzuDDU2QYJk9fIDOBTntF+yDv9LEA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies: peerDependencies:
typescript: '>=4.8.4 <6.1.0' typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/scope-manager@8.60.1': '@typescript-eslint/scope-manager@8.61.1':
resolution: {integrity: sha512-gvI5OQoptnxQnchOirukCuQ55svJSTuD/4k5+pC267xyBtYry748R9/c3tYUzb/iE6RZfllRz2lVulLCHkTm4w==} resolution: {integrity: sha512-L2bdIeoQS8FlKAvONAr20w6OcLXeB+qiDKbAooS9A0Ben+iSIkBef0FxqwKWYqt5sa0i4KJtxVyVmhMylKzF5w==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/tsconfig-utils@8.60.1': '@typescript-eslint/tsconfig-utils@8.61.1':
resolution: {integrity: sha512-nh8w4qAteiKuZu3pSSzG/yGKpw0OlkrKnzFmbVRenKaD4qc+7i1GrmZaLVkr8rk4uipiPGMOW4YsM6WmKZ5CvA==} resolution: {integrity: sha512-UN/H4di+OO7EWx2ovME+8t31YO+KVnK0RRKEHR3kOt21/Ay8BOq3M1OMvWs5vNiqcFCYGYoxK3MXPZzmMUE+yg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies: peerDependencies:
typescript: '>=4.8.4 <6.1.0' typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/type-utils@8.60.1': '@typescript-eslint/type-utils@8.61.1':
resolution: {integrity: sha512-sdwTrpjosW7ANQYJ39ZBF1ZyEMEGVB2UsikrserVM/30a/F1dTLnu9bGxEdosugyu5caigjLrR2qiD11asjI1A==} resolution: {integrity: sha512-GYRicKmVK0C4fsKgaACaknOUAq9Oa2kwsjnpFhFcS/5p4Ht5IP9OVLbgIgcK4SRk92nVHFluurg1lumD9dBcLw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies: peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0' typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/types@8.60.1': '@typescript-eslint/types@8.61.1':
resolution: {integrity: sha512-4h0tY8ppCkdCzcrl2YM5M3my0xsE1Tf8om3owEu5oPWmXwkKRmk0j0LGDzYBGUcAlesEbxBhazqu/K4cu3Ug7w==} resolution: {integrity: sha512-G+CRlPqLv7Bz1IZVs03x5K59F1veqL0EJUROAdGhKsEq8qOiRiZbI+HUojPq5l0fEGOKModD9br6lObhB8zkoA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/typescript-estree@8.60.1': '@typescript-eslint/typescript-estree@8.61.1':
resolution: {integrity: sha512-alpRkfG8hlVE5kdJW2GkfgDgXxold3e8e4l6EnmhRmRLbekgAPCCGDVD++sABy9FcgPFroq+uFcCSM1vR57Cew==} resolution: {integrity: sha512-u+oQD3BqYWPc8YV9Zab4vaJElJuwOLPRc10Jm1o/qS+6Qwen14HCWwx0Seo4LnSn2wxea2Ik8DxPt2/FHmuhrg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies: peerDependencies:
typescript: '>=4.8.4 <6.1.0' typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/utils@8.60.1': '@typescript-eslint/utils@8.61.1':
resolution: {integrity: sha512-h2MPBLoNtjc3qZWfY3Tl51yPorQ2McHn8pJfcMNTcIvrrZrr90Ykffit0yjrPFWQcRcUxzH20+6OcVdW4yHtUg==} resolution: {integrity: sha512-1+P/3Dj6jvtybE1q0HQ6yBt/gq+oKJyLdEv4HdnqasaEXRSYCAsD59mXEVQnM/ULNdQxbX77tdG4jPRjIS6knA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies: peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0' typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/visitor-keys@8.60.1': '@typescript-eslint/visitor-keys@8.61.1':
resolution: {integrity: sha512-EbGRQg4FhrmwLodl+t3JNAnXHWVr9Vp+Zl1QBZVPY4ByfkzIT8cX3K6QWODHtkIZqqJVEWvhHSx3v5PDHsaQag==} resolution: {integrity: sha512-6fJ9MHWtK14C1DSkiMlHUSOmrVebL7150xZJBlJiL62jjhIA4JmOq6flwBgDxIdBKKdoiZRel+dfPD5MLfny3w==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@unblockneteasemusic/server@0.28.0': '@unblockneteasemusic/server@0.28.0':
@ -386,8 +386,8 @@ packages:
engines: {node: '>=0.4.0'} engines: {node: '>=0.4.0'}
hasBin: true hasBin: true
acorn@8.16.0: acorn@8.17.0:
resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==}
engines: {node: '>=0.4.0'} engines: {node: '>=0.4.0'}
hasBin: true hasBin: true
@ -485,8 +485,8 @@ packages:
resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
axios@1.17.0: axios@1.18.0:
resolution: {integrity: sha512-J8SwNxprqqpbfenehxWYXE7CW+wM1BB4w3+N+g+/Wx40xM4rsLrfPmHHxSWIxJLYDgSY/HqlFPIYb2/S3rxafw==} resolution: {integrity: sha512-E32NzpYKp++W7XRe52rHiXV2ehxmh3wbdgO7MHeFM+vqxLBYHzt0ElkiImtOBxtOmyp0yoC8C6uESVV84Y2/hw==}
balanced-match@1.0.2: balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
@ -513,8 +513,8 @@ packages:
resolution: {integrity: sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==} resolution: {integrity: sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==}
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
body-parser@2.2.2: body-parser@2.3.0:
resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} resolution: {integrity: sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==}
engines: {node: '>=18'} engines: {node: '>=18'}
brace-expansion@1.1.15: brace-expansion@1.1.15:
@ -864,6 +864,10 @@ packages:
resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==}
engines: {node: '>=18'} engines: {node: '>=18'}
es-abstract-get@1.0.0:
resolution: {integrity: sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==}
engines: {node: '>= 0.4'}
es-abstract@1.24.2: es-abstract@1.24.2:
resolution: {integrity: sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==} resolution: {integrity: sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@ -884,8 +888,8 @@ packages:
resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
es-to-primitive@1.3.0: es-to-primitive@1.3.1:
resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} resolution: {integrity: sha512-CxN9N56HYfd2m/acc/NOFrZQsN9kU4eh+2kk6A707Kz1krH8tKmfrs5RnftB8WNX80T0NS7vSQsDOlg23diR2g==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
es5-ext@0.10.64: es5-ext@0.10.64:
@ -1169,8 +1173,8 @@ packages:
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
engines: {node: '>=14'} engines: {node: '>=14'}
form-data@4.0.5: form-data@4.0.6:
resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
forwarded@0.2.0: forwarded@0.2.0:
@ -1203,8 +1207,8 @@ packages:
function-bind@1.1.2: function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
function.prototype.name@1.1.8: function.prototype.name@1.2.0:
resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==} resolution: {integrity: sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
functions-have-names@1.2.3: functions-have-names@1.2.3:
@ -1447,6 +1451,10 @@ packages:
resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
is-document.all@1.0.0:
resolution: {integrity: sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==}
engines: {node: '>= 0.4'}
is-extglob@2.1.1: is-extglob@2.1.1:
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@ -1636,6 +1644,10 @@ packages:
resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
media-typer@2.0.0:
resolution: {integrity: sha512-kOy3OxT2HH39N70UnKgu4NWDZjLOz8W/mfyvniHjRH/DrL3f2pOfvWQ4p60offbbtDAnXWp0v9LfMIqMec269Q==}
engines: {node: '>=18'}
merge-descriptors@1.0.3: merge-descriptors@1.0.3:
resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==}
@ -1729,8 +1741,8 @@ packages:
multistream@4.1.0: multistream@4.1.0:
resolution: {integrity: sha512-J1XDiAmmNpRCBfIWJv+n0ymC4ABcf/Pl+5YvC5B/D2f/2+8PtHvCNxMPKiQcZyi922Hq69J2YOpb1pTywfifyw==} resolution: {integrity: sha512-J1XDiAmmNpRCBfIWJv+n0ymC4ABcf/Pl+5YvC5B/D2f/2+8PtHvCNxMPKiQcZyi922Hq69J2YOpb1pTywfifyw==}
music-metadata@11.12.3: music-metadata@11.13.0:
resolution: {integrity: sha512-n6hSTZkuD59qWgHh6IP5dtDlDZQXoxk/bcA85Jywg8Z1iFrlNgl2+GTFgjZyn52W5UgQpV42V4XqrQZZAMbZTQ==} resolution: {integrity: sha512-uXRaov9dfjSpQufXIU7sMxVZnh+FilCQv2mXn+K5EJ/decP3dTWrgvPYa5r6MtRbieNSCE708Da4J0u1UGfQIw==}
engines: {node: '>=18'} engines: {node: '>=18'}
napi-build-utils@1.0.2: napi-build-utils@1.0.2:
@ -1994,8 +2006,8 @@ packages:
resolution: {integrity: sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==} resolution: {integrity: sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==}
engines: {node: '>=6.0.0'} engines: {node: '>=6.0.0'}
prettier@3.8.3: prettier@3.8.4:
resolution: {integrity: sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==} resolution: {integrity: sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q==}
engines: {node: '>=14'} engines: {node: '>=14'}
hasBin: true hasBin: true
@ -2149,8 +2161,8 @@ packages:
secure-json-parse@2.7.0: secure-json-parse@2.7.0:
resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==} resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==}
semver@7.8.2: semver@7.8.5:
resolution: {integrity: sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ==} resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==}
engines: {node: '>=10'} engines: {node: '>=10'}
hasBin: true hasBin: true
@ -2211,8 +2223,8 @@ packages:
resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
side-channel@1.1.0: side-channel@1.1.1:
resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
signal-exit@4.1.0: signal-exit@4.1.0:
@ -2493,8 +2505,8 @@ packages:
undefsafe@2.0.5: undefsafe@2.0.5:
resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==} resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==}
undici-types@7.18.2: undici-types@7.24.6:
resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==}
universal-deep-strict-equal@1.2.2: universal-deep-strict-equal@1.2.2:
resolution: {integrity: sha512-UpnFi3/IF3jZHIHTdQXTHLCqpBP3805OFFRPHgvCS7k0oob2YVXxMTjS0U0g9qJTzqFRMwEnFFSlFLqt6zwjTQ==} resolution: {integrity: sha512-UpnFi3/IF3jZHIHTdQXTHLCqpBP3805OFFRPHgvCS7k0oob2YVXxMTjS0U0g9qJTzqFRMwEnFFSlFLqt6zwjTQ==}
@ -2631,12 +2643,12 @@ packages:
resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==}
engines: {node: '>=8'} engines: {node: '>=8'}
yargs@16.2.0: yargs@16.2.2:
resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} resolution: {integrity: sha512-Nt9ZJjXTv5R8MHbqby/wXQ6Gi0Bb3TcYZkR1bzuL4yB2OxWPkXknz513gEF0GoA6tn00UpbPvERW8rzCuWCA6w==}
engines: {node: '>=10'} engines: {node: '>=10'}
yargs@17.7.2: yargs@17.7.3:
resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} resolution: {integrity: sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==}
engines: {node: '>=12'} engines: {node: '>=12'}
yargs@18.0.0: yargs@18.0.0:
@ -2756,10 +2768,10 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2 '@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/sourcemap-codec': 1.5.5
'@neteasecloudmusicapienhanced/unblockmusic-utils@0.3.2': '@neteasecloudmusicapienhanced/unblockmusic-utils@0.3.3':
dependencies: dependencies:
'@unblockneteasemusic/server': 0.28.0 '@unblockneteasemusic/server': 0.28.0
axios: 1.17.0 axios: 1.18.0
dotenv: 17.4.2 dotenv: 17.4.2
express: 4.22.2 express: 4.22.2
https: 1.0.0 https: 1.0.0
@ -2798,15 +2810,15 @@ snapshots:
'@types/body-parser@1.19.6': '@types/body-parser@1.19.6':
dependencies: dependencies:
'@types/connect': 3.4.38 '@types/connect': 3.4.38
'@types/node': 25.5.0 '@types/node': 25.9.2
'@types/busboy@1.5.4': '@types/busboy@1.5.4':
dependencies: dependencies:
'@types/node': 25.5.0 '@types/node': 25.9.2
'@types/connect@3.4.38': '@types/connect@3.4.38':
dependencies: dependencies:
'@types/node': 25.5.0 '@types/node': 25.9.2
'@types/estree@1.0.9': {} '@types/estree@1.0.9': {}
@ -2817,7 +2829,7 @@ snapshots:
'@types/express-serve-static-core@5.1.1': '@types/express-serve-static-core@5.1.1':
dependencies: dependencies:
'@types/node': 25.5.0 '@types/node': 25.9.2
'@types/qs': 6.15.1 '@types/qs': 6.15.1
'@types/range-parser': 1.2.7 '@types/range-parser': 1.2.7
'@types/send': 1.2.1 '@types/send': 1.2.1
@ -2834,9 +2846,9 @@ snapshots:
'@types/mocha@10.0.10': {} '@types/mocha@10.0.10': {}
'@types/node@25.5.0': '@types/node@25.9.2':
dependencies: dependencies:
undici-types: 7.18.2 undici-types: 7.24.6
'@types/qs@6.15.1': {} '@types/qs@6.15.1': {}
@ -2844,21 +2856,21 @@ snapshots:
'@types/send@1.2.1': '@types/send@1.2.1':
dependencies: dependencies:
'@types/node': 25.5.0 '@types/node': 25.9.2
'@types/serve-static@2.2.0': '@types/serve-static@2.2.0':
dependencies: dependencies:
'@types/http-errors': 2.0.5 '@types/http-errors': 2.0.5
'@types/node': 25.5.0 '@types/node': 25.9.2
'@typescript-eslint/eslint-plugin@8.60.1(@typescript-eslint/parser@8.60.1(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3)': '@typescript-eslint/eslint-plugin@8.61.1(@typescript-eslint/parser@8.61.1(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3)':
dependencies: dependencies:
'@eslint-community/regexpp': 4.12.2 '@eslint-community/regexpp': 4.12.2
'@typescript-eslint/parser': 8.60.1(eslint@9.39.4)(typescript@5.9.3) '@typescript-eslint/parser': 8.61.1(eslint@9.39.4)(typescript@5.9.3)
'@typescript-eslint/scope-manager': 8.60.1 '@typescript-eslint/scope-manager': 8.61.1
'@typescript-eslint/type-utils': 8.60.1(eslint@9.39.4)(typescript@5.9.3) '@typescript-eslint/type-utils': 8.61.1(eslint@9.39.4)(typescript@5.9.3)
'@typescript-eslint/utils': 8.60.1(eslint@9.39.4)(typescript@5.9.3) '@typescript-eslint/utils': 8.61.1(eslint@9.39.4)(typescript@5.9.3)
'@typescript-eslint/visitor-keys': 8.60.1 '@typescript-eslint/visitor-keys': 8.61.1
eslint: 9.39.4 eslint: 9.39.4
ignore: 7.0.5 ignore: 7.0.5
natural-compare: 1.4.0 natural-compare: 1.4.0
@ -2867,41 +2879,41 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@typescript-eslint/parser@8.60.1(eslint@9.39.4)(typescript@5.9.3)': '@typescript-eslint/parser@8.61.1(eslint@9.39.4)(typescript@5.9.3)':
dependencies: dependencies:
'@typescript-eslint/scope-manager': 8.60.1 '@typescript-eslint/scope-manager': 8.61.1
'@typescript-eslint/types': 8.60.1 '@typescript-eslint/types': 8.61.1
'@typescript-eslint/typescript-estree': 8.60.1(typescript@5.9.3) '@typescript-eslint/typescript-estree': 8.61.1(typescript@5.9.3)
'@typescript-eslint/visitor-keys': 8.60.1 '@typescript-eslint/visitor-keys': 8.61.1
debug: 4.4.3 debug: 4.4.3
eslint: 9.39.4 eslint: 9.39.4
typescript: 5.9.3 typescript: 5.9.3
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@typescript-eslint/project-service@8.60.1(typescript@5.9.3)': '@typescript-eslint/project-service@8.61.1(typescript@5.9.3)':
dependencies: dependencies:
'@typescript-eslint/tsconfig-utils': 8.60.1(typescript@5.9.3) '@typescript-eslint/tsconfig-utils': 8.61.1(typescript@5.9.3)
'@typescript-eslint/types': 8.60.1 '@typescript-eslint/types': 8.61.1
debug: 4.4.3 debug: 4.4.3
typescript: 5.9.3 typescript: 5.9.3
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@typescript-eslint/scope-manager@8.60.1': '@typescript-eslint/scope-manager@8.61.1':
dependencies: dependencies:
'@typescript-eslint/types': 8.60.1 '@typescript-eslint/types': 8.61.1
'@typescript-eslint/visitor-keys': 8.60.1 '@typescript-eslint/visitor-keys': 8.61.1
'@typescript-eslint/tsconfig-utils@8.60.1(typescript@5.9.3)': '@typescript-eslint/tsconfig-utils@8.61.1(typescript@5.9.3)':
dependencies: dependencies:
typescript: 5.9.3 typescript: 5.9.3
'@typescript-eslint/type-utils@8.60.1(eslint@9.39.4)(typescript@5.9.3)': '@typescript-eslint/type-utils@8.61.1(eslint@9.39.4)(typescript@5.9.3)':
dependencies: dependencies:
'@typescript-eslint/types': 8.60.1 '@typescript-eslint/types': 8.61.1
'@typescript-eslint/typescript-estree': 8.60.1(typescript@5.9.3) '@typescript-eslint/typescript-estree': 8.61.1(typescript@5.9.3)
'@typescript-eslint/utils': 8.60.1(eslint@9.39.4)(typescript@5.9.3) '@typescript-eslint/utils': 8.61.1(eslint@9.39.4)(typescript@5.9.3)
debug: 4.4.3 debug: 4.4.3
eslint: 9.39.4 eslint: 9.39.4
ts-api-utils: 2.5.0(typescript@5.9.3) ts-api-utils: 2.5.0(typescript@5.9.3)
@ -2909,37 +2921,37 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@typescript-eslint/types@8.60.1': {} '@typescript-eslint/types@8.61.1': {}
'@typescript-eslint/typescript-estree@8.60.1(typescript@5.9.3)': '@typescript-eslint/typescript-estree@8.61.1(typescript@5.9.3)':
dependencies: dependencies:
'@typescript-eslint/project-service': 8.60.1(typescript@5.9.3) '@typescript-eslint/project-service': 8.61.1(typescript@5.9.3)
'@typescript-eslint/tsconfig-utils': 8.60.1(typescript@5.9.3) '@typescript-eslint/tsconfig-utils': 8.61.1(typescript@5.9.3)
'@typescript-eslint/types': 8.60.1 '@typescript-eslint/types': 8.61.1
'@typescript-eslint/visitor-keys': 8.60.1 '@typescript-eslint/visitor-keys': 8.61.1
debug: 4.4.3 debug: 4.4.3
minimatch: 10.2.5 minimatch: 10.2.5
semver: 7.8.2 semver: 7.8.5
tinyglobby: 0.2.17 tinyglobby: 0.2.17
ts-api-utils: 2.5.0(typescript@5.9.3) ts-api-utils: 2.5.0(typescript@5.9.3)
typescript: 5.9.3 typescript: 5.9.3
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@typescript-eslint/utils@8.60.1(eslint@9.39.4)(typescript@5.9.3)': '@typescript-eslint/utils@8.61.1(eslint@9.39.4)(typescript@5.9.3)':
dependencies: dependencies:
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4) '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4)
'@typescript-eslint/scope-manager': 8.60.1 '@typescript-eslint/scope-manager': 8.61.1
'@typescript-eslint/types': 8.60.1 '@typescript-eslint/types': 8.61.1
'@typescript-eslint/typescript-estree': 8.60.1(typescript@5.9.3) '@typescript-eslint/typescript-estree': 8.61.1(typescript@5.9.3)
eslint: 9.39.4 eslint: 9.39.4
typescript: 5.9.3 typescript: 5.9.3
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@typescript-eslint/visitor-keys@8.60.1': '@typescript-eslint/visitor-keys@8.61.1':
dependencies: dependencies:
'@typescript-eslint/types': 8.60.1 '@typescript-eslint/types': 8.61.1
eslint-visitor-keys: 5.0.1 eslint-visitor-keys: 5.0.1
'@unblockneteasemusic/server@0.28.0': '@unblockneteasemusic/server@0.28.0':
@ -2960,13 +2972,13 @@ snapshots:
acorn-es7-plugin@1.1.7: {} acorn-es7-plugin@1.1.7: {}
acorn-jsx@5.3.2(acorn@8.16.0): acorn-jsx@5.3.2(acorn@8.17.0):
dependencies: dependencies:
acorn: 8.16.0 acorn: 8.17.0
acorn@5.7.4: {} acorn@5.7.4: {}
acorn@8.16.0: {} acorn@8.17.0: {}
agent-base@6.0.2: agent-base@6.0.2:
dependencies: dependencies:
@ -3056,10 +3068,10 @@ snapshots:
dependencies: dependencies:
possible-typed-array-names: 1.1.0 possible-typed-array-names: 1.1.0
axios@1.17.0: axios@1.18.0:
dependencies: dependencies:
follow-redirects: 1.16.0 follow-redirects: 1.16.0
form-data: 4.0.5 form-data: 4.0.6
https-proxy-agent: 5.0.1 https-proxy-agent: 5.0.1
proxy-from-env: 2.1.0 proxy-from-env: 2.1.0
transitivePeerDependencies: transitivePeerDependencies:
@ -3099,10 +3111,10 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
body-parser@2.2.2: body-parser@2.3.0:
dependencies: dependencies:
bytes: 3.1.2 bytes: 3.1.2
content-type: 1.0.5 content-type: 2.0.0
debug: 4.4.3 debug: 4.4.3
http-errors: 2.0.1 http-errors: 2.0.1
iconv-lite: 0.7.2 iconv-lite: 0.7.2
@ -3464,6 +3476,13 @@ snapshots:
environment@1.1.0: {} environment@1.1.0: {}
es-abstract-get@1.0.0:
dependencies:
es-errors: 1.3.0
es-object-atoms: 1.1.2
is-callable: 1.2.7
object-inspect: 1.13.4
es-abstract@1.24.2: es-abstract@1.24.2:
dependencies: dependencies:
array-buffer-byte-length: 1.0.2 array-buffer-byte-length: 1.0.2
@ -3478,8 +3497,8 @@ snapshots:
es-errors: 1.3.0 es-errors: 1.3.0
es-object-atoms: 1.1.2 es-object-atoms: 1.1.2
es-set-tostringtag: 2.1.0 es-set-tostringtag: 2.1.0
es-to-primitive: 1.3.0 es-to-primitive: 1.3.1
function.prototype.name: 1.1.8 function.prototype.name: 1.2.0
get-intrinsic: 1.3.0 get-intrinsic: 1.3.0
get-proto: 1.0.1 get-proto: 1.0.1
get-symbol-description: 1.1.0 get-symbol-description: 1.1.0
@ -3536,8 +3555,10 @@ snapshots:
has-tostringtag: 1.0.2 has-tostringtag: 1.0.2
hasown: 2.0.4 hasown: 2.0.4
es-to-primitive@1.3.0: es-to-primitive@1.3.1:
dependencies: dependencies:
es-abstract-get: 1.0.0
es-errors: 1.3.0
is-callable: 1.2.7 is-callable: 1.2.7
is-date-object: 1.1.0 is-date-object: 1.1.0
is-symbol: 1.1.1 is-symbol: 1.1.1
@ -3630,10 +3651,10 @@ snapshots:
dependencies: dependencies:
htmlparser2: 10.1.0 htmlparser2: 10.1.0
eslint-plugin-prettier@5.5.6(eslint-config-prettier@10.1.8(eslint@9.39.4))(eslint@9.39.4)(prettier@3.8.3): eslint-plugin-prettier@5.5.6(eslint-config-prettier@10.1.8(eslint@9.39.4))(eslint@9.39.4)(prettier@3.8.4):
dependencies: dependencies:
eslint: 9.39.4 eslint: 9.39.4
prettier: 3.8.3 prettier: 3.8.4
prettier-linter-helpers: 1.0.1 prettier-linter-helpers: 1.0.1
synckit: 0.11.13 synckit: 0.11.13
optionalDependencies: optionalDependencies:
@ -3739,8 +3760,8 @@ snapshots:
espree@10.4.0: espree@10.4.0:
dependencies: dependencies:
acorn: 8.16.0 acorn: 8.17.0
acorn-jsx: 5.3.2(acorn@8.16.0) acorn-jsx: 5.3.2(acorn@8.17.0)
eslint-visitor-keys: 4.2.1 eslint-visitor-keys: 4.2.1
esprima@2.7.3: {} esprima@2.7.3: {}
@ -3819,7 +3840,7 @@ snapshots:
express@5.2.1: express@5.2.1:
dependencies: dependencies:
accepts: 2.0.0 accepts: 2.0.0
body-parser: 2.2.2 body-parser: 2.3.0
content-disposition: 1.1.0 content-disposition: 1.1.0
content-type: 1.0.5 content-type: 1.0.5
cookie: 0.7.2 cookie: 0.7.2
@ -3953,7 +3974,7 @@ snapshots:
cross-spawn: 7.0.6 cross-spawn: 7.0.6
signal-exit: 4.1.0 signal-exit: 4.1.0
form-data@4.0.5: form-data@4.0.6:
dependencies: dependencies:
asynckit: 0.4.0 asynckit: 0.4.0
combined-stream: 1.0.8 combined-stream: 1.0.8
@ -3986,14 +4007,17 @@ snapshots:
function-bind@1.1.2: {} function-bind@1.1.2: {}
function.prototype.name@1.1.8: function.prototype.name@1.2.0:
dependencies: dependencies:
call-bind: 1.0.9 call-bind: 1.0.9
call-bound: 1.0.4 call-bound: 1.0.4
define-properties: 1.2.1 es-define-property: 1.0.1
es-errors: 1.3.0
functions-have-names: 1.2.3 functions-have-names: 1.2.3
has-property-descriptors: 1.0.2
hasown: 2.0.4 hasown: 2.0.4
is-callable: 1.2.7 is-callable: 1.2.7
is-document.all: 1.0.0
functions-have-names@1.2.3: {} functions-have-names@1.2.3: {}
@ -4183,7 +4207,7 @@ snapshots:
dependencies: dependencies:
es-errors: 1.3.0 es-errors: 1.3.0
hasown: 2.0.4 hasown: 2.0.4
side-channel: 1.1.0 side-channel: 1.1.1
into-stream@6.0.0: into-stream@6.0.0:
dependencies: dependencies:
@ -4247,6 +4271,10 @@ snapshots:
call-bound: 1.0.4 call-bound: 1.0.4
has-tostringtag: 1.0.2 has-tostringtag: 1.0.2
is-document.all@1.0.0:
dependencies:
call-bound: 1.0.4
is-extglob@2.1.1: {} is-extglob@2.1.1: {}
is-finalizationregistry@1.1.1: is-finalizationregistry@1.1.1:
@ -4428,6 +4456,8 @@ snapshots:
media-typer@1.1.0: {} media-typer@1.1.0: {}
media-typer@2.0.0: {}
merge-descriptors@1.0.3: {} merge-descriptors@1.0.3: {}
merge-descriptors@2.0.0: {} merge-descriptors@2.0.0: {}
@ -4501,7 +4531,7 @@ snapshots:
strip-json-comments: 3.1.1 strip-json-comments: 3.1.1
supports-color: 8.1.1 supports-color: 8.1.1
workerpool: 9.3.4 workerpool: 9.3.4
yargs: 17.7.2 yargs: 17.7.3
yargs-parser: 21.1.1 yargs-parser: 21.1.1
yargs-unparser: 2.0.0 yargs-unparser: 2.0.0
@ -4520,14 +4550,14 @@ snapshots:
once: 1.4.0 once: 1.4.0
readable-stream: 3.6.2 readable-stream: 3.6.2
music-metadata@11.12.3: music-metadata@11.13.0:
dependencies: dependencies:
'@borewit/text-codec': 0.2.2 '@borewit/text-codec': 0.2.2
'@tokenizer/token': 0.3.0 '@tokenizer/token': 0.3.0
content-type: 1.0.5 content-type: 2.0.0
debug: 4.4.3 debug: 4.4.3
file-type: 21.3.4 file-type: 21.3.4
media-typer: 1.1.0 media-typer: 2.0.0
strtok3: 10.3.5 strtok3: 10.3.5
token-types: 6.1.2 token-types: 6.1.2
uint8array-extras: 1.5.0 uint8array-extras: 1.5.0
@ -4549,7 +4579,7 @@ snapshots:
node-abi@3.92.0: node-abi@3.92.0:
dependencies: dependencies:
semver: 7.8.2 semver: 7.8.5
node-fetch@2.7.0: node-fetch@2.7.0:
dependencies: dependencies:
@ -4560,7 +4590,7 @@ snapshots:
node-windows@1.0.0-beta.8: node-windows@1.0.0-beta.8:
dependencies: dependencies:
xml: 1.0.1 xml: 1.0.1
yargs: 17.7.2 yargs: 17.7.3
nodemon@3.1.14: nodemon@3.1.14:
dependencies: dependencies:
@ -4569,7 +4599,7 @@ snapshots:
ignore-by-default: 1.0.1 ignore-by-default: 1.0.1
minimatch: 10.2.5 minimatch: 10.2.5
pstree.remy: 1.1.8 pstree.remy: 1.1.8
semver: 7.8.2 semver: 7.8.5
simple-update-notifier: 2.0.0 simple-update-notifier: 2.0.0
supports-color: 5.5.0 supports-color: 5.5.0
touch: 3.1.1 touch: 3.1.1
@ -4744,9 +4774,9 @@ snapshots:
https-proxy-agent: 5.0.1 https-proxy-agent: 5.0.1
node-fetch: 2.7.0 node-fetch: 2.7.0
progress: 2.0.3 progress: 2.0.3
semver: 7.8.2 semver: 7.8.5
tar-fs: 2.1.4 tar-fs: 2.1.4
yargs: 16.2.0 yargs: 16.2.2
transitivePeerDependencies: transitivePeerDependencies:
- encoding - encoding
- supports-color - supports-color
@ -4864,7 +4894,7 @@ snapshots:
dependencies: dependencies:
fast-diff: 1.3.0 fast-diff: 1.3.0
prettier@3.8.3: {} prettier@3.8.4: {}
process-nextick-args@2.0.1: {} process-nextick-args@2.0.1: {}
@ -4896,7 +4926,7 @@ snapshots:
qs@6.15.2: qs@6.15.2:
dependencies: dependencies:
side-channel: 1.1.0 side-channel: 1.1.1
queue-microtask@1.2.3: {} queue-microtask@1.2.3: {}
@ -5038,7 +5068,7 @@ snapshots:
secure-json-parse@2.7.0: {} secure-json-parse@2.7.0: {}
semver@7.8.2: {} semver@7.8.5: {}
send@0.19.2: send@0.19.2:
dependencies: dependencies:
@ -5148,7 +5178,7 @@ snapshots:
object-inspect: 1.13.4 object-inspect: 1.13.4
side-channel-map: 1.0.1 side-channel-map: 1.0.1
side-channel@1.1.0: side-channel@1.1.1:
dependencies: dependencies:
es-errors: 1.3.0 es-errors: 1.3.0
object-inspect: 1.13.4 object-inspect: 1.13.4
@ -5168,7 +5198,7 @@ snapshots:
simple-update-notifier@2.0.0: simple-update-notifier@2.0.0:
dependencies: dependencies:
semver: 7.8.2 semver: 7.8.5
slash@3.0.0: {} slash@3.0.0: {}
@ -5471,7 +5501,7 @@ snapshots:
undefsafe@2.0.5: {} undefsafe@2.0.5: {}
undici-types@7.18.2: {} undici-types@7.24.6: {}
universal-deep-strict-equal@1.2.2: universal-deep-strict-equal@1.2.2:
dependencies: dependencies:
@ -5511,7 +5541,7 @@ snapshots:
which-builtin-type@1.2.1: which-builtin-type@1.2.1:
dependencies: dependencies:
call-bound: 1.0.4 call-bound: 1.0.4
function.prototype.name: 1.1.8 function.prototype.name: 1.2.0
has-tostringtag: 1.0.2 has-tostringtag: 1.0.2
is-async-function: 2.1.1 is-async-function: 2.1.1
is-date-object: 1.1.0 is-date-object: 1.1.0
@ -5628,7 +5658,7 @@ snapshots:
y18n: 4.0.3 y18n: 4.0.3
yargs-parser: 18.1.3 yargs-parser: 18.1.3
yargs@16.2.0: yargs@16.2.2:
dependencies: dependencies:
cliui: 7.0.4 cliui: 7.0.4
escalade: 3.2.0 escalade: 3.2.0
@ -5638,7 +5668,7 @@ snapshots:
y18n: 5.0.8 y18n: 5.0.8
yargs-parser: 20.2.9 yargs-parser: 20.2.9
yargs@17.7.2: yargs@17.7.3:
dependencies: dependencies:
cliui: 8.0.1 cliui: 8.0.1
escalade: 3.2.0 escalade: 3.2.0

View File

@ -11,4 +11,4 @@
[前往本家](https://github.com/binaryify/NeteaseCloudMusicApi) [前往本家](https://github.com/binaryify/NeteaseCloudMusicApi)
[快速开始](#neteasecloudmusicapienhanced) [快速开始](#neteasecloudmusicapienhanced)
![color](#ffffff) ![color](#ffffff)

View File

@ -88,6 +88,7 @@ v4.29.9 加入了生成随机中国 IP 功能, 在请求时加上 `randomCNIP=tr
5. 直接点`Continue` 5. 直接点`Continue`
6. `PROJECT NAME`自己填,`FRAMEWORK PRESET``Other` 然后直接点 `Deploy` 接着等部署完成即可 6. `PROJECT NAME`自己填,`FRAMEWORK PRESET``Other` 然后直接点 `Deploy` 接着等部署完成即可
## 腾讯云 serverless 部署 ## 腾讯云 serverless 部署
`Vercel` 在国内访问太慢(不绑定自己的域名的情况下),在此提供腾讯云 serverless 部署方法 `Vercel` 在国内访问太慢(不绑定自己的域名的情况下),在此提供腾讯云 serverless 部署方法
@ -101,6 +102,7 @@ v4.29.9 加入了生成随机中国 IP 功能, 在请求时加上 `randomCNIP=tr
5. 输入`应用名`,上传方式选择`代码仓库`,进行 GitHub 授权(如已授权可跳过这一步),代码仓库选择刚刚 fork 的项目 5. 输入`应用名`,上传方式选择`代码仓库`,进行 GitHub 授权(如已授权可跳过这一步),代码仓库选择刚刚 fork 的项目
6. 启动文件填入: 6. 启动文件填入:
``` ```
#!/bin/bash #!/bin/bash
export PORT=9000 export PORT=9000
@ -113,6 +115,7 @@ export PORT=9000
- 腾讯云 serverless 并不是免费的,前三个月有免费额度,之后收费 - 腾讯云 serverless 并不是免费的,前三个月有免费额度,之后收费
- 当前(2024-08-24), 用此法创建的话, 会`默认`关联一个"日志服务-日志主题"(创建过程中没有提醒), 此服务是计量收费的 - 当前(2024-08-24), 用此法创建的话, 会`默认`关联一个"日志服务-日志主题"(创建过程中没有提醒), 此服务是计量收费的
## 可以使用代理 ## 可以使用代理
在 query 参数中加上 proxy=your-proxy 即可让这一次的请求使用 proxy 在 query 参数中加上 proxy=your-proxy 即可让这一次的请求使用 proxy
@ -180,6 +183,7 @@ request 相关的环境变量
5. no_proxy 5. no_proxy
6. NO_PROXY 6. NO_PROXY
```shell ```shell
docker pull moefurina/ncm-api docker pull moefurina/ncm-api
@ -214,6 +218,7 @@ $ sudo docker run -d -p 3000:3000 netease-music-api
- 需要返回值加密时, 可传 `e_r=1`, `weapi``eapi` 都支持 - 需要返回值加密时, 可传 `e_r=1`, `weapi``eapi` 都支持
- 目前支持算法 有 `weapi`, `eapi`, `linuxapi``xeapi` (xeapi 是一种不加密的特殊算法, 主要用于调试加密前的原始请求参数) - 目前支持算法 有 `weapi`, `eapi`, `linuxapi``xeapi` (xeapi 是一种不加密的特殊算法, 主要用于调试加密前的原始请求参数)
## 接口文档 ## 接口文档
### 调用前须知 ### 调用前须知
@ -268,12 +273,12 @@ AI 生成的图,仅供娱乐()
#### 1. 手机登录 #### 1. 手机登录
**必选参数 :** **必选参数 :**
`phone`: 手机号码 `phone`: 手机号码
`password`: 密码 `password`: 密码
**可选参数 :** **可选参数 :**
`countrycode`: 国家码,用于国外手机号登录,例如美国传入:`1` `countrycode`: 国家码,用于国外手机号登录,例如美国传入:`1`
`md5_password`: md5 加密后的密码,传入后 `password` 参数将失效 `md5_password`: md5 加密后的密码,传入后 `password` 参数将失效
@ -432,11 +437,11 @@ body {
### 检测手机号码是否已注册 ### 检测手机号码是否已注册
说明 : 调用此接口 ,可检测手机号码是否已注册 说明 : 调用此接口 ,可检测手机号码是否已注册
**必选参数 :** **必选参数 :**
`phone` : 手机号码 `phone` : 手机号码
**可选参数 :** **可选参数 :**
`countrycode`: 国家码,用于国外手机号,例如美国传入:`1` ,默认 86 即中国 `countrycode`: 国家码,用于国外手机号,例如美国传入:`1` ,默认 86 即中国
**接口地址 :** `/cellphone/existence/check` **接口地址 :** `/cellphone/existence/check`
@ -445,7 +450,7 @@ body {
### 初始化昵称 ### 初始化昵称
说明 : 刚注册的账号(需登录),调用此接口 ,可初始化昵称 说明 : 刚注册的账号(需登录),调用此接口 ,可初始化昵称
**必选参数 :** **必选参数 :**
`nickname` : 昵称 `nickname` : 昵称
@ -700,7 +705,7 @@ tags: 歌单标签
说明 : 登录后调用此接口,使用`'Content-Type': 'multipart/form-data'`上传图片 formData(name 为'imgFile'),可更新歌单封面(参考:https://github.com/neteasecloudmusicapienhanced/api-enhanced/blob/main/public/playlist_cover_update.html) 说明 : 登录后调用此接口,使用`'Content-Type': 'multipart/form-data'`上传图片 formData(name 为'imgFile'),可更新歌单封面(参考:https://github.com/neteasecloudmusicapienhanced/api-enhanced/blob/main/public/playlist_cover_update.html)
**必选参数 :** **必选参数 :**
`id`: 歌单 id 3143833470 `id`: 歌单 id 3143833470
**可选参数 :** **可选参数 :**
@ -730,7 +735,7 @@ tags: 歌单标签
说明 : 登录后调用此接口,可以根据歌曲 id 顺序调整歌曲顺序 说明 : 登录后调用此接口,可以根据歌曲 id 顺序调整歌曲顺序
**必选参数 :** **必选参数 :**
`pid`: 歌单 id `pid`: 歌单 id
`ids`: 歌曲 id 列表 `ids`: 歌曲 id 列表
@ -1210,6 +1215,7 @@ tags: 歌单标签
> 如果你设置 limit=50&offset=100你就会得到第 101-150 首歌曲 > 如果你设置 limit=50&offset=100你就会得到第 101-150 首歌曲
### 歌单详情动态 ### 歌单详情动态
说明 : 调用后可获取歌单详情动态部分,如评论数,是否收藏,播放数 说明 : 调用后可获取歌单详情动态部分,如评论数,是否收藏,播放数
@ -1403,7 +1409,7 @@ tags: 歌单标签
### 歌单收藏者 ### 歌单收藏者
说明 : 调用此接口 , 传入歌单 id 可获取歌单的所有收藏者 说明 : 调用此接口 , 传入歌单 id 可获取歌单的所有收藏者
**必选参数 :** **必选参数 :**
`id` : 歌单 id `id` : 歌单 id
@ -1496,6 +1502,7 @@ tags: 歌单标签
- 可能存在JSON 歌曲元数据 - 可能存在JSON 歌曲元数据
``` ```
{"t":0,"c":[{"tx":"作曲: "},{"tx":"柳重言","li":"http://p1.music.126.net/Icj0IcaOjH2ZZpyAM-QGoQ==/6665239487822533.jpg","or":"orpheus://nm/artist/home?id=228547&type=artist"}]} {"t":0,"c":[{"tx":"作曲: "},{"tx":"柳重言","li":"http://p1.music.126.net/Icj0IcaOjH2ZZpyAM-QGoQ==/6665239487822533.jpg","or":"orpheus://nm/artist/home?id=228547&type=artist"}]}
{"t":5403,"c":[{"tx":"编曲: "},{"tx":"Alex San","li":"http://p1.music.126.net/pSbvYkrzZ1RFKqoh-fA9AQ==/109951166352922615.jpg","or":"orpheus://nm/artist/home?id=28984845&type=artist"}]} {"t":5403,"c":[{"tx":"编曲: "},{"tx":"Alex San","li":"http://p1.music.126.net/pSbvYkrzZ1RFKqoh-fA9AQ==/109951166352922615.jpg","or":"orpheus://nm/artist/home?id=28984845&type=artist"}]}
@ -1512,6 +1519,7 @@ tags: 歌单标签
* 逐字歌词 * 逐字歌词
``` ```
[16210,3460](16210,670,0)还(16880,410,0)没... [16210,3460](16210,670,0)还(16880,410,0)没...
~~~~1 ~~~2 ~~~~3 ~~4 5 ~6 (...) ~~~~1 ~~~2 ~~~~3 ~~4 5 ~6 (...)
@ -1588,7 +1596,7 @@ tags: 歌单标签
说明 : 调用此接口 , 传入资源 parentCommentId 和资源类型 type 和资源 id 参数, 可获得该资源的歌曲楼层评论 说明 : 调用此接口 , 传入资源 parentCommentId 和资源类型 type 和资源 id 参数, 可获得该资源的歌曲楼层评论
**必选参数 :** **必选参数 :**
`parentCommentId`: 楼层评论 id `parentCommentId`: 楼层评论 id
`id` : 资源 id `id` : 资源 id
@ -1804,7 +1812,7 @@ tags: 歌单标签
说明 : 调用此接口 , 传入资源类型和资源 id,以及排序方式,可获取对应资源的评论 说明 : 调用此接口 , 传入资源类型和资源 id,以及排序方式,可获取对应资源的评论
**必选参数 :** **必选参数 :**
`id` : 资源 id, 如歌曲 id,mv id `id` : 资源 id, 如歌曲 id,mv id
`type`: 数字 , 资源类型 , 对应歌曲 , mv, 专辑 , 歌单 , 电台, 视频对应以下类型 `type`: 数字 , 资源类型 , 对应歌曲 , mv, 专辑 , 歌单 , 电台, 视频对应以下类型
@ -1827,7 +1835,7 @@ tags: 歌单标签
7: 电台 7: 电台
``` ```
**可选参数 :** **可选参数 :**
`pageNo`:分页参数,第 N 页,默认为 1 `pageNo`:分页参数,第 N 页,默认为 1
`pageSize`:分页参数,每页多少条数据,默认 20 `pageSize`:分页参数,每页多少条数据,默认 20
@ -2200,7 +2208,7 @@ privilege:权限相关信息
说明 : 调用此接口 , 可获得已收藏专辑列表 说明 : 调用此接口 , 可获得已收藏专辑列表
**可选参数 :** **可选参数 :**
`limit`: 取出数量 , 默认为 25 `limit`: 取出数量 , 默认为 25
`offset`: 偏移数量 , 用于分页 , 如 :( 页数 -1)\*25, 其中 25 为 limit 的值 , 默认 `offset`: 偏移数量 , 用于分页 , 如 :( 页数 -1)\*25, 其中 25 为 limit 的值 , 默认
@ -2492,6 +2500,18 @@ privilege:权限相关信息
**调用例子 :** `/scrobble?id=518066366&sourceid=36780169&time=291` **调用例子 :** `/scrobble?id=518066366&sourceid=36780169&time=291`
#### 听歌打卡 V2 (NCBL 加密版)
说明 : 调用此接口,使用桌面客户端 NCBL 加密日志格式上报听歌记录
**必选参数 :** `id`: 歌曲 id, `time`: 播放时长(秒)
**可选参数 :** `sourceid`: 来源列表 id, `source`: 来源名称(默认 list), `name`: 歌曲名, `artist`: 艺术家, `bitrate`: 码率(默认 320), `level`: 音质等级(默认 exhigh), `total`: 歌曲总时长(秒)
**接口地址 :** `/scrobble/v1`
**调用例子 :** `/scrobble/v1?id=518066366&sourceid=36780169&time=291`
### 提交歌曲播放状态 ### 提交歌曲播放状态
说明 : 调用此接口可提交歌曲播放状态,支持会话追踪和播放模式记录,未传入 `sessionId` 时后端会自动生成 说明 : 调用此接口可提交歌曲播放状态,支持会话追踪和播放模式记录,未传入 `sessionId` 时后端会自动生成
@ -2521,7 +2541,7 @@ privilege:权限相关信息
说明 : 调用此接口 , 可获取全部 mv 说明 : 调用此接口 , 可获取全部 mv
**可选参数 :** **可选参数 :**
`area`: 地区,可选值为全部,内地,港台,欧美,日本,韩国,不填则为全部 `area`: 地区,可选值为全部,内地,港台,欧美,日本,韩国,不填则为全部
`type`: 类型,可选值为全部,官方版,原生,现场版,网易出品,不填则为全部 `type`: 类型,可选值为全部,官方版,原生,现场版,网易出品,不填则为全部
@ -2603,7 +2623,7 @@ privilege:权限相关信息
**接口地址 :** `/program/recommend` **接口地址 :** `/program/recommend`
**可选参数 :** **可选参数 :**
`limit`: 取出数量 , 默认为 10 `limit`: 取出数量 , 默认为 10
`offset`: 偏移数量 , 用于分页 , 如 :( 页数 -1)\*10, 其中 10 为 limit 的值 , 默认 `offset`: 偏移数量 , 用于分页 , 如 :( 页数 -1)\*10, 其中 10 为 limit 的值 , 默认
@ -2885,6 +2905,7 @@ type : 地区
- 适合 Vercel、Netlify 等有请求体限制的平台 - 适合 Vercel、Netlify 等有请求体限制的平台
- 需要前端配合实现 - 需要前端配合实现
#### 客户端直传相关接口 #### 客户端直传相关接口
**获取上传凭证** **获取上传凭证**
@ -2931,6 +2952,7 @@ type : 地区
- `artist`: 艺术家 - `artist`: 艺术家
- `album`: 专辑名 - `album`: 专辑名
#### 客户端直传流程 #### 客户端直传流程
1. 客户端计算文件 MD5 1. 客户端计算文件 MD5
@ -2938,11 +2960,12 @@ type : 地区
3. 如果 `needUpload` 为 true,直接 PUT 文件到 `uploadUrl` 3. 如果 `needUpload` 为 true,直接 PUT 文件到 `uploadUrl`
4. 调用 `/cloud/upload/complete` 完成导入 4. 调用 `/cloud/upload/complete` 完成导入
### 云盘歌曲信息匹配纠正 ### 云盘歌曲信息匹配纠正
说明 : 登录后调用此接口,可对云盘歌曲信息匹配纠正,如需取消匹配,asid 需要传 0 说明 : 登录后调用此接口,可对云盘歌曲信息匹配纠正,如需取消匹配,asid 需要传 0
**必选参数 :** **必选参数 :**
`uid`: 用户 id `uid`: 用户 id
`sid`: 云盘的歌曲 id `sid`: 云盘的歌曲 id
@ -3413,7 +3436,7 @@ type='1009' 获取其 id, 如`/search?keywords= 代码时间 &type=1009`
`limit` : 返回数量 , 默认为 30 `limit` : 返回数量 , 默认为 30
`offset` : 偏移数量,用于分页 , 如 :( 页数 -1)\*30, 其中 30 为 limit 的值 , 默认为 0 `offset` : 偏移数量,用于分页 , 如 :( 页数 -1)\*30, 其中 30 为 limit 的值 , 默认为 0
**接口地址 :** `/album/list` **接口地址 :** `/album/list`
**调用例子 :** `/album/list?limit=10` **调用例子 :** `/album/list?limit=10`
@ -3572,7 +3595,7 @@ type='1009' 获取其 id, 如`/search?keywords= 代码时间 &type=1009`
**可选参数 :** `limit`: 取出评论数量 , 默认为 10 **可选参数 :** `limit`: 取出评论数量 , 默认为 10
`offset`: 偏移数量 , 用于分页 , 如 :( 评论页数 -1)\*10, 其中 10 为 limit 的值 `offset`: 偏移数量 , 用于分页 , 如 :( 评论页数 -1)\*10, 其中 10 为 limit 的值
**接口地址 :** `/yunbei/tasks/expense` **接口地址 :** `/yunbei/tasks/expense`
**调用例子 :** `/yunbei/tasks/expense?limit=1` **调用例子 :** `/yunbei/tasks/expense?limit=1`
@ -4210,6 +4233,7 @@ ONLINE 已发布
- `voiceFeeType: 0`:返回免费的声音 - `voiceFeeType: 0`:返回免费的声音
- `voiceFeeType: 1`:返回收费的声音 - `voiceFeeType: 1`:返回收费的声音
### 播客声音详情 ### 播客声音详情
说明: 获取播客里的声音详情 说明: 获取播客里的声音详情
@ -5131,7 +5155,7 @@ let data = encodeURIComponent(
说明 : 登录后调用此接口, 获取我创建的博客声音 说明 : 登录后调用此接口, 获取我创建的博客声音
**可选参数 :** **可选参数 :**
`limit` : 返回数量 , 默认为 20 `limit` : 返回数量 , 默认为 20
@ -5144,7 +5168,7 @@ let data = encodeURIComponent(
说明: 调用此接口, 获取DIFM电台分类 说明: 调用此接口, 获取DIFM电台分类
**必选参数 :** **必选参数 :**
`sources`: 来源列表, 0: 最嗨电音 1: 古典电台 2: 爵士电台 `sources`: 来源列表, 0: 最嗨电音 1: 古典电台 2: 爵士电台
@ -5156,7 +5180,7 @@ let data = encodeURIComponent(
说明: 调用此接口, 获取DIFM电台收藏列表 说明: 调用此接口, 获取DIFM电台收藏列表
**必选参数 :** **必选参数 :**
`sources`: 来源列表, 0: 最嗨电音 1: 古典电台 2: 爵士电台 `sources`: 来源列表, 0: 最嗨电音 1: 古典电台 2: 爵士电台
@ -5192,7 +5216,7 @@ let data = encodeURIComponent(
说明: 调用此接口, 获取DIFM播放列表 说明: 调用此接口, 获取DIFM播放列表
**必选参数 :** **必选参数 :**
`source`: 来源, 0: 最嗨电音 1: 古典电台 2: 爵士电台 `source`: 来源, 0: 最嗨电音 1: 古典电台 2: 爵士电台
@ -5226,7 +5250,7 @@ let data = encodeURIComponent(
说明: 调用此接口, 获取标签下资源列表; 接口返回的`trackId`可以用于请求`/song/url/v1`接口,用于获取声音的下载地址 说明: 调用此接口, 获取标签下资源列表; 接口返回的`trackId`可以用于请求`/song/url/v1`接口,用于获取声音的下载地址
**必选参数 :** **必选参数 :**
`tag`: 标签, 由标签列表接口得到 `tag`: 标签, 由标签列表接口得到
@ -5238,7 +5262,7 @@ let data = encodeURIComponent(
说明: 调用此接口, 查看同类推荐 说明: 调用此接口, 查看同类推荐
**必选参数 :** **必选参数 :**
`id`: id, `/sati/tag/list`接口返回的`trackId` `id`: id, `/sati/tag/list`接口返回的`trackId`
@ -5258,7 +5282,7 @@ let data = encodeURIComponent(
说明: 调用此接口, 收藏声音 说明: 调用此接口, 收藏声音
**必选参数 :** **必选参数 :**
`id`: id, `/sati/tag/list`接口返回的`trackId` `id`: id, `/sati/tag/list`接口返回的`trackId`
@ -5274,7 +5298,7 @@ let data = encodeURIComponent(
说明: 调用此接口,获取跑步漫游的歌曲信息 说明: 调用此接口,获取跑步漫游的歌曲信息
**必选参数:** **必选参数:**
`bpm`: 步频 `bpm`: 步频

View File

@ -95,7 +95,8 @@ curl -s {origin}/search?keywords=网易云</code></pre>
<a href="/api_decrypt.html">API 解密</a> · <a href="/api_decrypt.html">API 解密</a> ·
<a href="/listen_together_host.html">一起听示例</a> · <a href="/listen_together_host.html">一起听示例</a> ·
<a href="/playlist_cover_update.html">更新歌单封面示例</a> · <a href="/playlist_cover_update.html">更新歌单封面示例</a> ·
<a href="/avatar_update.html">头像更新示例</a> <a href="/avatar_update.html">头像更新示例</a> ·
<a href="/scrobble.html">听歌打卡示例</a>
</div> </div>
</section> </section>

View File

@ -148,12 +148,12 @@
updateStatus('二维码已过期,请刷新页面', 'error') updateStatus('二维码已过期,请刷新页面', 'error')
clearInterval(timer) clearInterval(timer)
} else if (statusRes.code === 801) { } else if (statusRes.code === 801) {
updateStatus('二维码已扫描,请在手机上确认', 'waiting') updateStatus('等待手机扫码...', 'waiting')
} else if (statusRes.code === 802) { } else if (statusRes.code === 802) {
updateStatus('登录成功,正在保存信息...', 'waiting') updateStatus('二维码已扫描,请在手机上确认', 'waiting')
} else if (statusRes.code === 803) { } else if (statusRes.code === 803) {
clearInterval(timer) clearInterval(timer)
updateStatus('授权登录成功', 'success') updateStatus('授权登录成功,正在保存信息...', 'success')
await getLoginStatus(statusRes.cookie) await getLoginStatus(statusRes.cookie)
localStorage.setItem('cookie', statusRes.cookie) localStorage.setItem('cookie', statusRes.cookie)
} }

View File

@ -148,12 +148,12 @@
updateStatus('二维码已过期,请刷新页面', 'error') updateStatus('二维码已过期,请刷新页面', 'error')
clearInterval(timer) clearInterval(timer)
} else if (statusRes.code === 801) { } else if (statusRes.code === 801) {
updateStatus('二维码已扫描,请在手机上确认', 'waiting') updateStatus('等待手机扫码...', 'waiting')
} else if (statusRes.code === 802) { } else if (statusRes.code === 802) {
updateStatus('登录成功,正在保存信息...', 'waiting') updateStatus('二维码已扫描,请在手机上确认', 'waiting')
} else if (statusRes.code === 803) { } else if (statusRes.code === 803) {
clearInterval(timer) clearInterval(timer)
updateStatus('授权登录成功', 'success') updateStatus('授权登录成功,正在保存信息...', 'success')
await getLoginStatus(statusRes.cookie) await getLoginStatus(statusRes.cookie)
localStorage.setItem('cookie', statusRes.cookie) localStorage.setItem('cookie', statusRes.cookie)
} }

480
public/scrobble.html Normal file
View File

@ -0,0 +1,480 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>听歌打卡 - 网易云音乐 API Enhanced</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
min-height: 100vh;
background: #f5f5f5;
padding: 20px;
}
.container {
max-width: 520px;
margin: 40px auto;
background: white;
border-radius: 12px;
padding: 32px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
h1 {
font-size: 24px;
font-weight: 600;
color: #333;
margin-bottom: 8px;
text-align: center;
}
.subtitle {
font-size: 14px;
color: #666;
margin-bottom: 24px;
text-align: center;
}
.login-link {
display: block;
margin-bottom: 20px;
color: #666;
font-size: 14px;
text-decoration: none;
text-align: center;
}
.login-link:hover {
color: #333;
text-decoration: underline;
}
/* ---- Tabs ---- */
.tabs {
display: flex;
gap: 0;
margin-bottom: 24px;
border-bottom: 2px solid #eee;
}
.tab-btn {
flex: 1;
padding: 10px 0;
background: none;
border: none;
border-bottom: 2px solid transparent;
margin-bottom: -2px;
font-size: 14px;
font-weight: 500;
color: #999;
cursor: pointer;
transition: color 0.2s, border-color 0.2s;
}
.tab-btn:hover {
color: #555;
}
.tab-btn.active {
color: #333;
border-bottom-color: #333;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
/* ---- Form ---- */
.form-group {
margin-bottom: 16px;
text-align: left;
}
label {
display: block;
font-size: 13px;
font-weight: 500;
color: #555;
margin-bottom: 6px;
}
label .tag {
display: inline-block;
font-size: 11px;
font-weight: 400;
padding: 1px 6px;
border-radius: 3px;
margin-left: 6px;
}
label .tag.r {
background: #fee2e2;
color: #991b1b;
}
label .tag.o {
background: #fef3c7;
color: #92400e;
}
input[type="text"],
input[type="number"],
textarea {
width: 100%;
padding: 10px 14px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
outline: none;
font-family: inherit;
transition: border-color 0.2s;
}
input[type="text"]:focus,
input[type="number"]:focus,
textarea:focus {
border-color: #333;
}
textarea {
resize: vertical;
}
.row {
display: flex;
gap: 12px;
}
.row .form-group {
flex: 1;
}
.quick-fill {
margin-top: 5px;
font-size: 12px;
color: #666;
}
.quick-fill span {
color: #0066cc;
cursor: pointer;
margin-right: 8px;
}
.quick-fill span:hover {
text-decoration: underline;
}
.btn {
display: block;
width: 100%;
padding: 12px;
background: #333;
color: white;
font-size: 15px;
font-weight: 500;
border-radius: 6px;
cursor: pointer;
transition: background 0.2s ease;
border: none;
text-align: center;
margin-top: 20px;
}
.btn:hover {
background: #555;
}
.btn:disabled {
background: #ccc;
cursor: not-allowed;
}
.result {
margin-top: 16px;
padding: 12px 16px;
border-radius: 6px;
font-size: 14px;
text-align: left;
white-space: pre-wrap;
word-break: break-all;
font-family: monospace;
}
.result.success {
background: #d1fae5;
color: #065f46;
}
.result.error {
background: #fee2e2;
color: #991b1b;
}
.result.info {
background: #e0f2fe;
color: #0369a1;
}
.footer-link {
text-align: center;
margin-top: 24px;
font-size: 13px;
}
.footer-link a {
color: #666;
text-decoration: none;
}
.footer-link a:hover {
text-decoration: underline;
}
</style>
</head>
<body>
<div class="container">
<h1>听歌打卡</h1>
<p class="subtitle">同步听歌记录至网易云音乐,增加听歌排行计数</p>
<a href="/qrlogin-nocookie.html" class="login-link">还没登录?点击登录</a>
<!-- Cookie (共享) -->
<div class="form-group">
<label for="cookie">Cookie <span class="tag o">可选</span></label>
<textarea id="cookie" placeholder="留空则默认读取本地存储的登录态" rows="2"></textarea>
<div class="quick-fill">
<span onclick="loadLocalCookie()">读取本地 Cookie</span>
</div>
</div>
<!-- Tabs -->
<div class="tabs">
<button class="tab-btn active" data-tab="v0" onclick="switchTab('v0')">原版</button>
<button class="tab-btn" data-tab="v1" onclick="switchTab('v1')">V2 (NCBL 加密版)</button>
</div>
<!-- ============ Tab: v0 (原版 /scrobble) ============ -->
<div class="tab-content active" id="tab-v0">
<div class="form-group">
<label>歌曲 ID <span class="tag r">必填</span></label>
<input type="text" id="v0-songId" placeholder="例如: 2756058128" value="2756058128" />
<div class="quick-fill">
示例:
<span onclick="fillV0('2756058128', '288651229')">妖精小姐的魔法邀约</span>
<span onclick="fillV0('2637402867', '251025018')">Echoes of Memoria</span>
<span onclick="fillV0('36307815', '3394198')">Lose Control</span>
</div>
</div>
<div class="form-group">
<label>来源 ID (歌单或专辑 ID) <span class="tag r">必填</span></label>
<input type="text" id="v0-sourceId" placeholder="例如: 2756058128" value="2756058128" />
</div>
<div class="form-group">
<label>播放时间 (秒) <span class="tag o">可选</span></label>
<input type="number" id="v0-time" placeholder="300" value="300" />
</div>
<button class="btn" onclick="submitV0()">立即打卡 (原版)</button>
</div>
<!-- ============ Tab: v1 (NCBL 加密版 /scrobble/v1) ============ -->
<div class="tab-content" id="tab-v1">
<div class="form-group">
<label>歌曲 ID <span class="tag r">必填</span></label>
<input type="text" id="v1-songId" placeholder="例如: 518066366" value="518066366" />
<div class="quick-fill">
示例:
<span onclick="fillV1('518066366', '36780169', '291')">默认示例</span>
<span onclick="fillV1('2756058128', '288651229', '300')">妖精小姐的魔法邀约</span>
<span onclick="fillV1('2637402867', '251025018', '300')">Echoes of Memoria</span>
</div>
</div>
<div class="form-group">
<label>播放时间 (秒) <span class="tag r">必填</span></label>
<input type="number" id="v1-time" placeholder="例如: 291" value="291" />
</div>
<div class="form-group">
<label>来源 ID <span class="tag o">可选</span></label>
<input type="text" id="v1-sourceId" placeholder="歌单或专辑 ID" value="36780169" />
</div>
<div class="row">
<div class="form-group">
<label>歌曲名 <span class="tag o">可选</span></label>
<input type="text" id="v1-name" placeholder="歌曲名称" />
</div>
<div class="form-group">
<label>艺术家 <span class="tag o">可选</span></label>
<input type="text" id="v1-artist" placeholder="歌手名" />
</div>
</div>
<div class="row">
<div class="form-group">
<label>码率 <span class="tag o">可选</span></label>
<input type="number" id="v1-bitrate" placeholder="默认 320" value="320" />
</div>
<div class="form-group">
<label>音质等级 <span class="tag o">可选</span></label>
<input type="text" id="v1-level" placeholder="默认 exhigh" value="exhigh" />
</div>
</div>
<div class="form-group">
<label>歌曲总时长 (秒) <span class="tag o">可选</span></label>
<input type="number" id="v1-total" placeholder="留空则与播放时间相同" />
</div>
<button class="btn" onclick="submitV1()">立即打卡 (NCBL 加密版)</button>
</div>
<!-- Result -->
<div id="result" class="result" style="display: none;"></div>
<div class="footer-link">
<a href="/">返回首页</a>
</div>
</div>
<script src="https://fastly.jsdelivr.net/npm/axios@0.26.1/dist/axios.min.js"></script>
<script>
/* ---- DOM refs ---- */
const cookieInput = document.getElementById('cookie')
const resultDiv = document.getElementById('result')
/* ---- init ---- */
if (localStorage.getItem('cookie')) {
cookieInput.value = localStorage.getItem('cookie')
}
/* ---- Tab switching ---- */
function switchTab(name) {
document.querySelectorAll('.tab-btn').forEach(b => b.classList.toggle('active', b.dataset.tab === name))
document.querySelectorAll('.tab-content').forEach(c => c.classList.toggle('active', c.id === 'tab-' + name))
hideResult()
}
/* ---- Cookie ---- */
function loadLocalCookie() {
const local = localStorage.getItem('cookie')
if (local) {
cookieInput.value = local
showResult('已成功读取本地 Cookie', 'success')
} else {
showResult('未在本地发现登录 Cookie请先登录或手动粘贴', 'error')
}
}
/* ---- Quick fill ---- */
function fillV0(id, sourceId) {
document.getElementById('v0-songId').value = id
document.getElementById('v0-sourceId').value = sourceId
}
function fillV1(id, sourceId, time) {
document.getElementById('v1-songId').value = id
document.getElementById('v1-sourceId').value = sourceId
document.getElementById('v1-time').value = time
}
/* ---- Result ---- */
function hideResult() {
resultDiv.style.display = 'none'
}
function showResult(message, type) {
resultDiv.textContent = message
resultDiv.className = 'result ' + type
resultDiv.style.display = 'block'
}
function disableAll(disabled) {
document.querySelectorAll('.btn').forEach(b => b.disabled = disabled)
}
/* ---- Submit: 原版 /scrobble ---- */
async function submitV0() {
const id = document.getElementById('v0-songId').value.trim()
const sourceid = document.getElementById('v0-sourceId').value.trim()
const time = document.getElementById('v0-time').value.trim() || '300'
if (!id) { showResult('请输入歌曲 ID ', 'error'); return }
if (!sourceid) { showResult('请输入来源 ID ', 'error'); return }
disableAll(true)
showResult('打卡中,请稍候...', 'info')
try {
const params = { id, sourceid: sourceid || id, time, timestamp: Date.now() }
const cookie = cookieInput.value.trim()
if (cookie) params.cookie = cookie
const res = await axios({ method: 'get', url: '/scrobble', params })
if (res.data.code === 200) {
showResult('✅ 打卡成功! \n\n' + JSON.stringify(res.data, null, 2), 'success')
} else {
showResult('请求已发送,返回:\n\n' + JSON.stringify(res.data, null, 2), 'info')
}
} catch (error) {
const errData = error.response ? error.response.data : { error: error.message }
showResult('❌ 打卡失败\n\n' + JSON.stringify(errData, null, 2), 'error')
} finally {
disableAll(false)
}
}
/* ---- Submit: NCBL 加密版 /scrobble/v1 ---- */
async function submitV1() {
const id = document.getElementById('v1-songId').value.trim()
const time = document.getElementById('v1-time').value.trim()
if (!id) { showResult('请输入歌曲 ID ', 'error'); return }
if (!time || isNaN(+time) || +time <= 0) { showResult('请输入有效的播放时长(秒)!', 'error'); return }
disableAll(true)
showResult('打卡中NCBL 加密),请稍候...', 'info')
try {
const params = {
id,
time,
sourceid: document.getElementById('v1-sourceId').value.trim() || undefined,
name: document.getElementById('v1-name').value.trim() || undefined,
artist: document.getElementById('v1-artist').value.trim() || undefined,
bitrate: document.getElementById('v1-bitrate').value.trim() || undefined,
level: document.getElementById('v1-level').value.trim() || undefined,
total: document.getElementById('v1-total').value.trim() || undefined,
timestamp: Date.now(),
}
const cookie = cookieInput.value.trim()
if (cookie) params.cookie = cookie
// 清理 undefined 参数
Object.keys(params).forEach(k => params[k] === undefined && delete params[k])
const res = await axios({ method: 'get', url: '/scrobble/v1', params })
if (res.data.code === 200) {
showResult('✅ NCBL 加密打卡成功! \n\n' + JSON.stringify(res.data, null, 2), 'success')
} else {
showResult('请求已发送,返回:\n\n' + JSON.stringify(res.data, null, 2), 'info')
}
} catch (error) {
const errData = error.response ? error.response.data : { error: error.message }
showResult('❌ 打卡失败\n\n' + JSON.stringify(errData, null, 2), 'error')
} finally {
disableAll(false)
}
}
</script>
</body>
</html>

577
scripts/format-docs.js Normal file
View File

@ -0,0 +1,577 @@
#!/usr/bin/env node
/**
* 📝 Markdown 文档格式化工具喵~
* 支持格式化标题代码块空行和缩进
*
* 用法:
* node scripts/format-docs.js # 格式化默认文档 (public/docs/home.md)
* node scripts/format-docs.js <文件路径> # 格式化指定文件
* node scripts/format-docs.js --dir <目录> # 格式化整个目录的 .md 文件
* node scripts/format-docs.js --check # 只检查不写入 (dry-run)
*
*/
const fs = require('fs')
const path = require('path')
// ======================== 配置 ========================
const CONFIG = {
maxConsecutiveBlankLines: 2, // 最大连续空行数
codeBlockLang: true, // 代码块是否保留语言标记
headingSpaceBefore: true, // 标题前是否确保空行
headingSpaceAfter: true, // 标题后是否确保空行
listIndent: 2, // 列表缩进空格数
encodeSpecialChars: false, // 是否编码特殊字符
removeTrailingSpaces: true, // 是否删除行尾空格
}
const DEFAULT_FILE = path.resolve(__dirname, '..', 'public', 'docs', 'home.md')
// ======================== 颜色工具 ========================
const color = (code) => (s) => `\x1b[${code}m${s}\x1b[0m`
const green = color('32')
const cyan = color('36')
const yellow = color('33')
const red = color('31')
const bold = color('1')
const dim = color('2')
// ======================== 核心格式化函数 ========================
/**
* 解析 Markdown 的块结构返回块数组
* 块类型: 'heading', 'code', 'list', 'paragraph', 'empty', 'hr', 'blockquote', 'table', 'html'
*/
function parseBlocks(lines) {
const blocks = []
let i = 0
while (i < lines.length) {
const line = lines[i]
const trimmed = line.trim()
// 空行
if (trimmed === '') {
blocks.push({ type: 'empty', lines: [line], raw: line })
i++
continue
}
// 代码块 (``` 或 ~~~)
if (/^```/.test(trimmed) || /^~~~/.test(trimmed)) {
const marker = trimmed.match(/^(```|~~~)/)[1]
const lang = trimmed.slice(marker.length).trim()
const start = i
i++
while (i < lines.length && !lines[i].trim().startsWith(marker)) {
i++
}
if (i < lines.length) i++ // 跳过结束标记
const codeLines = lines.slice(start, i)
blocks.push({
type: 'code',
lines: codeLines,
lang,
marker,
raw: codeLines.join('\n'),
})
continue
}
// 标题
const headingMatch = trimmed.match(/^(#{1,6})\s+(.+)$/)
if (headingMatch) {
const level = headingMatch[1].length
blocks.push({
type: 'heading',
lines: [line],
level,
text: headingMatch[2],
raw: line,
})
i++
continue
}
// 分割线
if (/^(-{3,}|\*{3,}|_{3,})\s*$/.test(trimmed)) {
blocks.push({ type: 'hr', lines: [line], raw: line })
i++
continue
}
// HTML 注释或标签
if (/^<!--/.test(trimmed) || /^<\w+/.test(trimmed)) {
const start = i
i++
while (i < lines.length) {
const t = lines[i].trim()
if (/-->/.test(t) || /<\/\w+>/.test(t)) {
i++
break
}
i++
}
const htmlLines = lines.slice(start, i)
blocks.push({ type: 'html', lines: htmlLines, raw: htmlLines.join('\n') })
continue
}
// 引用块 (注: 空行不吞噬,留给后续解析,避免干扰上下文的空行判断)
if (/^>/.test(trimmed)) {
const start = i
i++
while (i < lines.length && lines[i].trimStart().startsWith('>')) {
i++
}
const quoteLines = lines.slice(start, i)
blocks.push({
type: 'blockquote',
lines: quoteLines,
raw: quoteLines.join('\n'),
})
continue
}
// 表格
if (
/\|/.test(trimmed) &&
lines[i + 1] &&
/^\|[\s\-:]+\|/.test(lines[i + 1].trim())
) {
const start = i
i += 2
while (i < lines.length && /\|/.test(lines[i].trim())) {
i++
}
const tableLines = lines.slice(start, i)
blocks.push({
type: 'table',
lines: tableLines,
raw: tableLines.join('\n'),
})
continue
}
// 列表项(有序或无序)
if (/^(\s*[-*+]\s|\s*\d+\.\s)/.test(line)) {
const start = i
i++
while (i < lines.length) {
const t = lines[i].trim()
if (t === '') {
i++
break
}
if (/^(\s*[-*+]\s|\s*\d+\.\s)/.test(lines[i])) {
i++
continue
}
// 缩进 continuation
if (/^\s{2,}/.test(lines[i])) {
i++
continue
}
break
}
const listLines = lines.slice(start, i)
blocks.push({ type: 'list', lines: listLines, raw: listLines.join('\n') })
continue
}
// 普通段落
const start = i
i++
while (i < lines.length && lines[i].trim() !== '') {
// 如果遇到新的块元素则停止
const t = lines[i].trim()
if (
/^#{1,6}\s/.test(t) ||
/^```/.test(t) ||
/^~~~/.test(t) ||
/^(\s*[-*+]\s|\s*\d+\.\s)/.test(lines[i])
)
break
// 分割线
if (/^(-{3,}|\*{3,}|_{3,})\s*$/.test(t)) break
i++
}
const paraLines = lines.slice(start, i)
blocks.push({
type: 'paragraph',
lines: paraLines,
raw: paraLines.join('\n'),
})
}
return blocks
}
/**
* 格式化文档
*/
function formatMarkdown(input, options = {}) {
const cfg = { ...CONFIG, ...options }
const lines = input.split('\n')
let blocks = parseBlocks(lines)
// ---------- 格式化步骤 ----------
// 1. 确保标题前后有空行
if (cfg.headingSpaceBefore || cfg.headingSpaceAfter) {
blocks = blocks.map((block, idx) => {
if (block.type !== 'heading') return block
const newLines = [...block.lines]
// 标题前加空行(如果不是第一个块且前一个不是空行)
if (
cfg.headingSpaceBefore &&
idx > 0 &&
blocks[idx - 1].type !== 'empty'
) {
newLines.unshift('')
}
// 标题后加空行(如果不是最后一个块且后一个不是空行)
if (
cfg.headingSpaceAfter &&
idx < blocks.length - 1 &&
blocks[idx + 1].type !== 'empty'
) {
newLines.push('')
}
return { ...block, lines: newLines }
})
}
// 2. 格式化代码块
blocks = blocks.map((block, idx) => {
if (block.type !== 'code') return block
const newLines = [...block.lines]
// 先提取代码块标记信息 (第一行是 ``` 或 ~~~)
const markerMatch = newLines[0].trim().match(/^(```|~~~)/)
if (!markerMatch) return block // 安全兜底
const marker = markerMatch[1]
const lang = newLines[0].trim().slice(marker.length).trim().toLowerCase()
// 标准化语言标记
const indent = newLines[0].match(/^\s*/)[0]
newLines[0] = lang ? `${indent}${marker}${lang}` : `${indent}${marker}`
// 确保代码块前后有空行(插在标准化之后,因为只动第一行)
if (
idx > 0 &&
blocks[idx - 1].type !== 'empty' &&
blocks[idx - 1].type !== 'code'
) {
newLines.unshift('')
}
if (
idx < blocks.length - 1 &&
blocks[idx + 1].type !== 'empty' &&
blocks[idx + 1].type !== 'code'
) {
newLines.push('')
}
return { ...block, lines: newLines }
})
// 3. 压缩多余空行
blocks = compressEmptyLines(blocks, cfg.maxConsecutiveBlankLines)
// 4. 删除行尾空格
if (cfg.removeTrailingSpaces) {
blocks = blocks.map((block) => ({
...block,
lines: block.lines.map((l) => l.replace(/[ \t]+$/, '')),
}))
}
// 5. 修复列表缩进
blocks = blocks.map((block) => {
if (block.type !== 'list') return block
return {
...block,
lines: block.lines.map((line) => {
const trimmed = line.trimStart()
// 检测列表标记
if (/^[-*+]\s/.test(trimmed) || /^\d+\.\s/.test(trimmed)) {
const indent = line.length - line.trimStart().length
// 如果是顶层列表项确保缩进为0
if (indent % cfg.listIndent !== 0) {
const normalizedIndent =
Math.round(indent / cfg.listIndent) * cfg.listIndent
return ' '.repeat(normalizedIndent) + trimmed
}
}
return line
}),
}
})
// 重新拼装
const resultLines = blocks.flatMap((b) => b.lines)
// 处理文件开头和结尾的空行
while (resultLines.length > 0 && resultLines[0] === '') resultLines.shift()
while (resultLines.length > 0 && resultLines[resultLines.length - 1] === '')
resultLines.pop()
resultLines.push('') // 文件结尾留一个空行
return resultLines.join('\n')
}
/**
* 压缩连续空行块
*/
function compressEmptyLines(blocks, maxBlank) {
const result = []
let blankCount = 0
for (const block of blocks) {
if (block.type === 'empty') {
blankCount++
if (blankCount <= maxBlank) {
result.push(block)
}
} else {
blankCount = 0
result.push(block)
}
}
return result
}
// ======================== 统计信息 ========================
function getStats(input) {
const lines = input.split('\n')
const blocks = parseBlocks(lines)
return {
totalLines: lines.length,
nonEmptyLines: lines.filter((l) => l.trim() !== '').length,
headings: blocks.filter((b) => b.type === 'heading').length,
codeBlocks: blocks.filter((b) => b.type === 'code').length,
lists: blocks.filter((b) => b.type === 'list').length,
tables: blocks.filter((b) => b.type === 'table').length,
blockquotes: blocks.filter((b) => b.type === 'blockquote').length,
hr: blocks.filter((b) => b.type === 'hr').length,
characters: input.length,
}
}
function printStats(stats) {
console.log(bold('\n📊 文档统计信息:'))
console.log(` 总行数: ${cyan(String(stats.totalLines))}`)
console.log(` 非空行数: ${cyan(String(stats.nonEmptyLines))}`)
console.log(` 字符数: ${cyan(String(stats.characters))}`)
console.log(` 标题数: ${cyan(String(stats.headings))}`)
console.log(` 代码块数: ${cyan(String(stats.codeBlocks))}`)
console.log(` 列表数: ${cyan(String(stats.lists))}`)
console.log(` 表格数: ${cyan(String(stats.tables))}`)
console.log(` 引用块数: ${cyan(String(stats.blockquotes))}`)
console.log(` 分割线数: ${cyan(String(stats.hr))}`)
}
// ======================== 文件处理 ========================
function readFile(filePath) {
try {
let content = fs.readFileSync(filePath, 'utf-8')
// 统一换行符为 \n避免 Windows 的 \r\n 导致 diff 不稳定
content = content.replace(/\r\n/g, '\n').replace(/\r/g, '\n')
return content
} catch (err) {
console.error(red(`❌ 无法读取文件: ${filePath}`))
console.error(dim(err.message))
process.exit(1)
}
}
function writeFile(filePath, content) {
try {
fs.writeFileSync(filePath, content, 'utf-8')
console.log(green(`✅ 已写入: ${filePath}`))
} catch (err) {
console.error(red(`❌ 写入失败: ${filePath}`))
console.error(dim(err.message))
process.exit(1)
}
}
function processFile(filePath, options) {
const relativePath = path.relative(process.cwd(), filePath)
console.log(bold(`\n📄 处理文件: ${cyan(relativePath)}`))
const input = readFile(filePath)
const statsBefore = getStats(input)
if (options.verbose) {
console.log(dim(' 格式化前:'))
printStats(statsBefore)
}
const formatted = formatMarkdown(input, options)
const statsAfter = getStats(formatted)
const hasChanges = input !== formatted
if (options.check) {
if (hasChanges) {
console.log(yellow(' ⚠️ 文件需要格式化 (dry-run, 未写入)'))
const added = statsAfter.totalLines - statsBefore.totalLines
console.log(dim(` 行数变化: ${added > 0 ? '+' : ''}${added}`))
} else {
console.log(green(' ✅ 文件已格式良好'))
}
return hasChanges ? 1 : 0
}
if (hasChanges) {
writeFile(filePath, formatted)
console.log(green(' ✨ 格式化完成!'))
if (options.verbose) {
printStats(statsAfter)
}
} else {
console.log(green(' ✅ 文档已经格式良好,无需修改'))
}
return 0
}
function processDirectory(dirPath, options) {
let exitCode = 0
const files = fs
.readdirSync(dirPath)
.filter((f) => f.endsWith('.md'))
.map((f) => path.join(dirPath, f))
if (files.length === 0) {
console.log(yellow(`⚠️ 在 ${dirPath} 中未找到 .md 文件`))
return 0
}
console.log(
bold(`\n📁 扫描目录: ${cyan(dirPath)} (${files.length} 个文件)\n`),
)
for (const file of files) {
const ret = processFile(file, options)
if (ret !== 0) exitCode = ret
}
return exitCode
}
// ======================== CLI ========================
function printHelp() {
console.log(
bold(`
📝 Markdown 文档格式化工具 v1.0.0
${cyan('用法:')}
node scripts/format-docs.js [文件路径] [选项]
${cyan('参数:')}
文件路径 要格式化的 .md 文件 (默认: public/docs/home.md)
${cyan('选项:')}
--dir, -d <目录> 格式化整个目录下的所有 .md 文件
--check, -c dry-run 模式,只检查不写入
--verbose, -v 显示详细统计信息
--help, -h 显示帮助信息
${cyan('示例:')}
node scripts/format-docs.js
node scripts/format-docs.js README.md
node scripts/format-docs.js --dir docs/
node scripts/format-docs.js --check
`),
)
}
function parseArgs() {
const args = process.argv.slice(2)
const options = {
file: null,
dir: null,
check: false,
verbose: false,
help: false,
}
for (let i = 0; i < args.length; i++) {
switch (args[i]) {
case '--dir':
case '-d':
options.dir = args[++i]
break
case '--check':
case '-c':
options.check = true
break
case '--verbose':
case '-v':
options.verbose = true
break
case '--help':
case '-h':
options.help = true
break
default:
if (!args[i].startsWith('--') && !args[i].startsWith('-')) {
options.file = path.resolve(process.cwd(), args[i])
}
}
}
return options
}
// ======================== 主函数 ========================
function main() {
const options = parseArgs()
if (options.help) {
printHelp()
return 0
}
console.log(bold(`\n${cyan('🐱 文档格式化工具')} ${dim('(用❤️制作)')}\n`))
if (options.dir) {
const dirPath = path.resolve(process.cwd(), options.dir)
if (!fs.existsSync(dirPath)) {
console.error(red(`❌ 目录不存在: ${dirPath}`))
return 1
}
return processDirectory(dirPath, options)
}
const filePath = options.file || DEFAULT_FILE
if (!fs.existsSync(filePath)) {
console.error(red(`❌ 文件不存在: ${filePath}`))
return 1
}
return processFile(filePath, options)
}
// ======================== 启动 ========================
if (require.main === module) {
const exitCode = main()
process.exit(exitCode)
}
module.exports = { formatMarkdown, getStats, parseBlocks }

View File

@ -10,6 +10,7 @@ const { cookieToJson } = require('./util/index')
const fileUpload = require('express-fileupload') const fileUpload = require('express-fileupload')
const decode = require('safe-decode-uri-component') const decode = require('safe-decode-uri-component')
const logger = require('./util/logger.js') const logger = require('./util/logger.js')
const { APP_CONF } = require('./util/config.json')
/** /**
* The version check result. * The version check result.
@ -299,15 +300,15 @@ async function constructServer(moduleDefs) {
) )
try { try {
let usedCrypto = ''
const moduleResponse = await moduleDef.module(query, (...params) => { const moduleResponse = await moduleDef.module(query, (...params) => {
// 参数注入客户端IP
const obj = [...params] const obj = [...params]
const options = obj[2] || {} const options = obj[2] || {}
usedCrypto = options.crypto || ''
let ip = '' let ip = ''
if (options.randomCNIP) { if (options.randomCNIP) {
ip = global.cnIp ip = global.cnIp
// logger.info('Using random Chinese IP for request:', ip)
} else { } else {
ip = req.ip ip = req.ip
@ -317,7 +318,6 @@ async function constructServer(moduleDefs) {
if (ip == '::1') { if (ip == '::1') {
ip = global.cnIp ip = global.cnIp
} }
// logger.info('Requested from ip:', ip)
} }
obj[2] = { obj[2] = {
@ -327,7 +327,10 @@ async function constructServer(moduleDefs) {
return request(...obj) return request(...obj)
}) })
logger.info(`Request Success: ${decode(req.originalUrl)}`) const displayCrypto = usedCrypto || (APP_CONF.encrypt ? 'eapi' : 'api')
logger.info(
`Request Success: [${displayCrypto}] ${decode(req.originalUrl)}`,
)
// 夹带私货部分如果开启了通用解锁并且是获取歌曲URL的接口则尝试解锁如果需要的话ヾ(≧▽≦*)o // 夹带私货部分如果开启了通用解锁并且是获取歌曲URL的接口则尝试解锁如果需要的话ヾ(≧▽≦*)o
if ( if (
@ -422,7 +425,7 @@ async function serveNcmApi(options) {
options.checkVersion && options.checkVersion &&
checkVersion().then(({ npmVersion, ourVersion, status }) => { checkVersion().then(({ npmVersion, ourVersion, status }) => {
if (status == VERSION_CHECK_RESULT.NOT_LATEST) { if (status == VERSION_CHECK_RESULT.NOT_LATEST) {
logger.info( logger.warn(
`最新版本: ${npmVersion}, 当前版本: ${ourVersion}, 请及时更新`, `最新版本: ${npmVersion}, 当前版本: ${ourVersion}, 请及时更新`,
) )
} }
@ -444,11 +447,9 @@ async function serveNcmApi(options) {
`) `)
logger.info(` logger.info(
- Server started successfully @ http://${host ? host : 'localhost'}:${port} `Server started successfully @ http://${host ? host : 'localhost'}:${port}`,
- Environment: ${process.env.NODE_ENV || 'development'} )
- Node Version: ${process.version}
- Process ID: ${process.pid}`)
}) })
return appExt return appExt

View File

@ -13,6 +13,8 @@
"apiDomain": "https://interface.music.163.com", "apiDomain": "https://interface.music.163.com",
"xeapiDomain": "https://interface3.music.163.com", "xeapiDomain": "https://interface3.music.163.com",
"domain": "https://music.163.com", "domain": "https://music.163.com",
"clDomian": "https://clientlog.music.163.com",
"clDomian3": "https://clientlog3.music.163.com",
"encrypt": true, "encrypt": true,
"encryptResponse": false, "encryptResponse": false,
"clientSign": "18:C0:4D:B9:8F:FE@@@453832335F384641365F424635335F303030315F303031425F343434415F343643365F333638332@@@@@@6ff673ef74955b38bce2fa8562d95c976ed4758b1227c4e9ee345987cee17bc9", "clientSign": "18:C0:4D:B9:8F:FE@@@453832335F384641365F424635335F303030315F303031425F343434415F343643365F333638332@@@@@@6ff673ef74955b38bce2fa8562d95c976ed4758b1227c4e9ee345987cee17bc9",

View File

@ -62,9 +62,9 @@ const chinaIPRanges = (function loadChinaIPRanges() {
// attach total for convenience // attach total for convenience
arr.totalCount = total arr.totalCount = total
logger.info( // logger.info(
`Loaded ${arr.length} Chinese IP ranges from china_ip_ranges.txt, total ${total} IPs`, // `Loaded ${arr.length} Chinese IP ranges from china_ip_ranges.txt, total ${total} IPs`,
) // )
return arr return arr
} catch (error) { } catch (error) {
logger.error('Failed to load china_ip_ranges.txt:', error.message) logger.error('Failed to load china_ip_ranges.txt:', error.message)

481
util/ncbl.js Normal file
View File

@ -0,0 +1,481 @@
// NCBL 加密工具 —— 复制自 @netease-report-listen-song
// 提供 ChaCha20 / RSA-256 / NCBL 加密 / PLV+PLD 构建 / 设备上下文提取
const crypto = require('crypto')
const zlib = require('zlib')
const axios = require('axios')
const { APP_CONF } = require('./config.json')
const DOMAIN3 = APP_CONF.clDomian3
// ---- ChaCha20 纯 JS 实现 ----
const SIGMA = [0x61707865, 0x3320646e, 0x79622d32, 0x6b206574]
const rotl = (x, n) => ((x << n) | (x >>> (32 - n))) >>> 0
const quarterRound = (s, a, b, c, d) => {
s[a] = (s[a] + s[b]) >>> 0
s[d] ^= s[a]
s[d] = rotl(s[d], 16)
s[c] = (s[c] + s[d]) >>> 0
s[b] ^= s[c]
s[b] = rotl(s[b], 12)
s[a] = (s[a] + s[b]) >>> 0
s[d] ^= s[a]
s[d] = rotl(s[d], 8)
s[c] = (s[c] + s[d]) >>> 0
s[b] ^= s[c]
s[b] = rotl(s[b], 7)
}
const chachaBlock = (key, counter, nonce) => {
const state = new Uint32Array(16)
state[0] = SIGMA[0]
state[1] = SIGMA[1]
state[2] = SIGMA[2]
state[3] = SIGMA[3]
for (let i = 0; i < 8; i++) state[4 + i] = key.readUInt32LE(i * 4)
state[12] = counter >>> 0
state[13] = nonce.readUInt32LE(0)
state[14] = nonce.readUInt32LE(4)
state[15] = nonce.readUInt32LE(8)
const work = state.slice()
for (let i = 0; i < 10; i++) {
quarterRound(work, 0, 4, 8, 12)
quarterRound(work, 1, 5, 9, 13)
quarterRound(work, 2, 6, 10, 14)
quarterRound(work, 3, 7, 11, 15)
quarterRound(work, 0, 5, 10, 15)
quarterRound(work, 1, 6, 11, 12)
quarterRound(work, 2, 7, 8, 13)
quarterRound(work, 3, 4, 9, 14)
}
const out = Buffer.allocUnsafe(64)
for (let i = 0; i < 16; i++)
out.writeUInt32LE((work[i] + state[i]) >>> 0, i * 4)
return out
}
const chacha20 = (key, counter, nonce, data) => {
const out = Buffer.allocUnsafe(data.length)
for (let off = 0; off < data.length; off += 64) {
const ks = chachaBlock(key, (counter + (off >>> 6)) >>> 0, nonce)
const end = Math.min(off + 64, data.length)
for (let i = off; i < end; i++) out[i] = data[i] ^ ks[i - off]
}
return out
}
// ---- RSA-256 Key Wrap (网易 NCBL 专用) ----
// 256-bit 模数 N 已被因式分解,故可直接计算私钥
const RSA_N =
0xfd90bd466ff9bc8a3fec2fbcf263b90d5c564879fa5d7aab89b31c1d5cb4139dn
const RSA_E = 65537n
const beToBig = (buf) => {
let n = 0n
for (const b of buf) n = (n << 8n) | BigInt(b)
return n
}
const bigToBe = (n, len) => {
const out = Buffer.alloc(len)
for (let i = len - 1; i >= 0; i--) {
out[i] = Number(n & 0xffn)
n >>= 8n
}
return out
}
const modPow = (base, exp, mod) => {
let result = 1n
base %= mod
while (exp > 0n) {
if (exp & 1n) result = (result * base) % mod
base = (base * base) % mod
exp >>= 1n
}
return result
}
const rsaWrap = (keyA) => {
return bigToBe(modPow(beToBig(keyA), RSA_E, RSA_N), 32)
}
// ---- NCBL 加密格式 ----
const MAGIC = Buffer.from('NCBL', 'ascii')
const NCBL_VERSION = 3
const HEADER_FIXED_LEN = 70
const META_BLOCK_TYPE = 0x4343
const DEFAULT_MAX_FRAME = 0x8000
const getCompress = () => {
if (typeof zlib.zstdCompressSync === 'function') {
return { compress: (buf) => zlib.zstdCompressSync(buf), name: 'zstd' }
}
return { compress: (buf) => zlib.gzipSync(buf), name: 'gzip' }
}
const encryptNCBL = (meta, body, opts = {}) => {
const metaBuf = Buffer.isBuffer(meta) ? meta : Buffer.from(meta, 'utf-8')
const bodyBuf = Buffer.isBuffer(body) ? body : Buffer.from(body, 'utf-8')
const maxFrame = opts.maxFrame || DEFAULT_MAX_FRAME
const keyA = opts.keyA || crypto.randomBytes(32)
if (keyA[0] >= 0xa3) keyA[0] = 0xa2
const keyB = rsaWrap(keyA)
const uuid = opts.uuid || crypto.randomBytes(16)
if (!opts.uuid) {
uuid[6] = (uuid[6] & 0x0f) | 0x40
uuid[8] = (uuid[8] & 0x3f) | 0x80
}
const nonce = uuid.subarray(0, 12)
const counter = uuid.readUInt32LE(12) >>> 2
const baseSeq = opts.baseSeq || crypto.randomBytes(2).readUInt16LE(0)
const metaCipher = chacha20(keyB, counter, nonce, metaBuf)
const metaBlock = Buffer.concat([
(() => {
const h = Buffer.allocUnsafe(4)
h.writeUInt16LE(META_BLOCK_TYPE, 0)
h.writeUInt16LE(metaCipher.length, 2)
return h
})(),
metaCipher,
])
const headerLen = HEADER_FIXED_LEN + metaBlock.length
const { compress } = getCompress()
const compressed = compress(bodyBuf)
const frames = []
let seq = baseSeq
for (let off = 0; off < compressed.length || off === 0; off += maxFrame) {
const slice = compressed.subarray(off, off + maxFrame)
const cipher = chacha20(keyA, counter, nonce, slice)
const head = Buffer.allocUnsafe(6)
head.writeUInt16LE(cipher.length, 0)
head.writeUInt32LE(seq >>> 0, 2)
frames.push(head, cipher)
seq++
if (compressed.length === 0) break
}
const trailing = Buffer.concat(frames)
const frameCount = seq - baseSeq
const header = Buffer.alloc(HEADER_FIXED_LEN)
MAGIC.copy(header, 0)
header.writeUInt32LE(NCBL_VERSION, 4)
header.writeUInt16LE(headerLen, 8)
uuid.copy(header, 10)
keyB.copy(header, 26)
header.writeUInt32LE(baseSeq >>> 0, 58)
header.writeUInt32LE((baseSeq + frameCount - 1) >>> 0, 62)
header.writeUInt32LE(trailing.length, 66)
return Buffer.concat([header, metaBlock, trailing])
}
// ---- 日志记录格式 ----
const FIELD_SEP = '\x01'
const buildRecord = ({ time, action, data }) => {
const json = typeof data === 'string' ? data : JSON.stringify(data)
return [time, action, json].join(FIELD_SEP)
}
const buildRecords = (records) => records.map(buildRecord).join('')
// ---- PLV / PLD 构建器 (桌面客户端格式) ----
const buildPlv = (ctx, song, source) => {
const now = Date.now()
const addRefer = `[F:63][${now}#933#${ctx.app.version}#${ctx.app.versionCode}#c9156c3][e][2][23][cell_pc_songlist_song:2|page_pc_songlist_songflow|page_mine_like_music][${song.id}:song:x:x|:::|${source.id}:list::]`
const multiRefers = [
'[F:26][s][18][_ai]',
'[F:26][s][12][_ai]',
`[F:63][${now}#933#${ctx.app.version}#${ctx.app.versionCode}#c9156c3][e][2][8][cell_pc_main_tab_entrance:6|page_pc_main_tab][我喜欢的音乐:spm::|:::]`,
'[F:26][s][5][_ai]',
'[F:26][s][0][_ai]',
]
return {
mode: 'circulation',
download: 0,
alg: '',
status: 'front',
id: String(song.id),
bitrate: song.bitrate,
type: 'song',
is_listentogether: 0,
source: source.name,
is_heart: 0,
resource_ratio: '',
resource_time: song.time,
musiceffect_id: '',
app_mode: 2,
bitrate_level: song.level,
_addrefer: addRefer,
_multirefers: multiRefers,
vipType: ctx.auth.vipType,
fee: 1,
file: 4,
rightSource: 0,
sourceId: source.id,
sourcetype: source.type,
libra_abt: '',
channel: ctx.app.channel,
curStartChannel: '',
}
}
const buildPld = (ctx, song, source, played) => {
const now = Date.now()
const addRefer = `[F:63][${now}#616#${ctx.app.version}#${ctx.app.versionCode}#c9156c3][e][2][92][btn_pc_cover_play|cell_pc_songlist_song:6|page_pc_songlist_songflow|page_mine_like_music][:::|${song.id}:song:x:x|:::|${source.id}:list::]`
const multiRefers = [
'[F:26][s][87][_ai]',
'[F:26][s][81][_ai]',
'[F:26][s][75][_ai]',
'[F:26][s][69][_ai]',
'[F:26][s][63][_ai]',
]
return {
mode: 'circulation',
download: 0,
alg: '',
status: 'front',
id: String(song.id),
time: played,
type: 'song',
is_listentogether: 0,
source: source.name,
is_heart: 0,
realtime: played,
resource_ratio: '',
resource_time: song.time,
musiceffect_id: '1001',
app_mode: 1,
lyriceffect: 'default',
displayMode: 'classic',
bitrate: song.bitrate,
bitrate_level: song.level,
_addrefer: addRefer,
_multirefers: multiRefers,
vipType: ctx.auth.vipType,
fee: 8,
file: 4,
rightSource: 0,
sourceId: source.id,
sourcetype: source.type,
end: 'interrupt',
libra_abt: '',
channel: ctx.app.channel,
curStartChannel: '',
}
}
// ---- Cookie → 设备/认证上下文 转换 ----
const extractContext = (cookieObj) => {
return {
app: {
id: cookieObj.appid || '',
urs: '',
pid: '',
nsm: cookieObj.WEVNSM || '1.0.0',
cid:
cookieObj.WNMCID ||
`${crypto.randomBytes(3).toString('hex')}.${Date.now()}.01.0`,
channel: cookieObj.channel || 'netease',
version: cookieObj.appver || '3.1.35',
versionCode: cookieObj.versioncode || '205293',
buildCode: cookieObj.buildver || '',
buildType: 'release',
packageId: '',
},
device: {
id: cookieObj.deviceId || cookieObj.sDeviceId || '',
ti: cookieObj.NMTID || '',
sign: cookieObj.clientSign || '',
model: cookieObj.mode || cookieObj.mobilename || '',
nnid: cookieObj._ntes_nnid || ',',
nuid: cookieObj._ntes_nuid || '',
csrf: cookieObj.__csrf || '',
systemType: cookieObj.os || 'pc',
systemVersion:
cookieObj.osver ||
'Microsoft-Windows-10-Professional-build-19045-64bit',
},
auth: {
id: cookieObj.uid || '',
token: cookieObj.MUSIC_U || '',
sessionId: cookieObj['JSESSIONID-WYYY'] || '',
vipType: cookieObj.vipType || '',
},
startTime: Date.now(),
processId: Math.floor(Math.random() * 90000) + 10000,
}
}
// 从 query.cookie 解析 cookie 对象
// 支持字符串 ("key=val; key2=val2") 或已解析的对象
// 注意:不做 URL decodeMUSIC_U 中的 '+' 会被 decodeURIComponent 错误地转为空格
const parseCookie = (cookie) => {
if (typeof cookie === 'object' && cookie !== null) return cookie
if (typeof cookie === 'string') {
const obj = {}
cookie.split(';').forEach((part) => {
const idx = part.indexOf('=')
if (idx > 0) {
const key = part.substring(0, idx).trim()
const val = part.substring(idx + 1).trim()
if (key) obj[key] = val
}
})
return obj
}
return {}
}
// ---- 辅助函数 ----
const randomUUID = () => {
if (typeof crypto.randomUUID === 'function') {
return crypto.randomUUID().replace(/-/g, '')
}
return 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'.replace(/x/g, () =>
Math.floor(Math.random() * 16).toString(16),
)
}
const randomHex = (len) => crypto.randomBytes(len / 2).toString('hex')
// ---- HTTP 上传工具 (NCBL 专用) ----
const buildMultipart = (payload) => {
const boundary = randomUUID()
const fileName = `op_${Math.floor(Math.random() * 90000) + 10000}_0_${Math.floor(Math.random() * 4294967295) + 1}`
const CRLF = '\r\n'
const headerLines = [
`--${boundary}`,
`Content-Disposition: form-data; name="file"; filename="${fileName}"`,
'Content-Type: multipart/form-data',
'',
'',
].join(CRLF)
const footer = `${CRLF}--${boundary}--${CRLF}`
return {
boundary,
fileName,
multipartBody: Buffer.concat([
Buffer.from(headerLines, 'utf-8'),
payload,
Buffer.from(footer, 'utf-8'),
]),
}
}
const buildCookieStr = (ctx) => {
const parts = [
`JSESSIONID-WYYY=${ctx.auth.sessionId}`,
`MUSIC_U=${ctx.auth.token}`,
`NMTID=${ctx.device.ti}`,
`WEVNSM=${ctx.app.nsm}`,
`WNMCID=${ctx.app.cid}`,
`__csrf=${ctx.device.csrf}`,
`__remember_me=true`,
`_iuqxldmzr_=33`,
`_ntes_nnid=${ctx.device.nnid}`,
`_ntes_nuid=${ctx.device.nuid}`,
`appver=${ctx.app.version}.${ctx.app.versionCode}`,
`channel=${ctx.app.channel}`,
`clientSign=${ctx.device.sign}`,
`deviceId=${ctx.device.id}`,
`mode=${ctx.device.model}`,
`ntes_kaola_ad=1`,
`os=${ctx.device.systemType}`,
`osver=${ctx.device.systemVersion}`,
]
return parts.join('; ')
}
const buildMetaJson = (ctx) =>
JSON.stringify({
'JSESSIONID-WYYY': ctx.auth.sessionId,
MUSIC_U: ctx.auth.token,
NMTID: ctx.device.ti,
WEVNSM: ctx.app.nsm,
WNMCID: ctx.app.cid,
__csrf: ctx.device.csrf,
_iuqxldmzr_: '33',
_ntes_nnid: ctx.device.nnid,
_ntes_nuid: ctx.device.nuid,
appver: `${ctx.app.version}.${ctx.app.versionCode}`,
channel: ctx.app.channel,
clientSign: ctx.device.sign,
deviceId: ctx.device.id,
mode: ctx.device.model,
ntes_kaola_ad: '1',
os: ctx.device.systemType,
osver: ctx.device.systemVersion,
})
const doUpload = async (ctx, metaJson, body, cookieStr, label) => {
const uploadUrl = DOMAIN3 + '/api/clientlog/encrypt/upload?multiupload=true'
const payload = encryptNCBL(metaJson, body)
const { boundary, fileName, multipartBody } = buildMultipart(payload)
const resp = await axios({
method: 'POST',
url: uploadUrl,
headers: {
'Content-Type': `multipart/form-data; boundary=${boundary}`,
Referer: 'https://music.163.com/di',
'User-Agent': `Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Safari/537.36 Chrome/91.0.4472.164 NeteaseMusicDesktop/${ctx.app.version}`,
'Accept-Encoding': 'gzip,deflate',
'Accept-Language': 'zh-CN,zh;q=0.8',
Cookie: cookieStr,
},
data: multipartBody,
maxBodyLength: 10 * 1024 * 1024,
timeout: 15000,
validateStatus: () => true,
})
const respBody = resp.data
const code = respBody?.code
const success =
code === 200 && respBody?.data?.successfiles?.includes?.(fileName)
return { success, fileName, payload, respBody }
}
// ---- 导出 ----
module.exports = {
chacha20,
rsaWrap,
encryptNCBL,
getCompress,
MAGIC,
NCBL_VERSION,
HEADER_FIXED_LEN,
META_BLOCK_TYPE,
DEFAULT_MAX_FRAME,
buildRecord,
buildRecords,
FIELD_SEP,
buildPlv,
buildPld,
extractContext,
parseCookie,
randomUUID,
randomHex,
buildMultipart,
buildCookieStr,
buildMetaJson,
doUpload,
}

View File

@ -88,6 +88,12 @@ const osMap = {
osver: '16.2', osver: '16.2',
channel: 'distribution', channel: 'distribution',
}, },
osx: {
os: 'osx',
appver: '3.1.10.5100',
osver: '15.5',
channel: 'netease',
},
} }
// 预先定义userAgentMap // 预先定义userAgentMap
@ -306,7 +312,11 @@ const createRequest = (uri, data, options) => {
if (cookie.MUSIC_A) header['MUSIC_A'] = cookie.MUSIC_A if (cookie.MUSIC_A) header['MUSIC_A'] = cookie.MUSIC_A
headers['Cookie'] = createHeaderCookie(header) headers['Cookie'] = createHeaderCookie(header)
headers['User-Agent'] = options.ua || chooseUserAgent('api', 'iphone') headers['User-Agent'] =
options.ua ||
(cookie.os === 'osx'
? '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'
: chooseUserAgent('api', 'iphone'))
if (crypto === 'eapi') { if (crypto === 'eapi') {
// headers['x-aeapi'] = true // 服务器会使用gzip压缩返回值 // headers['x-aeapi'] = true // 服务器会使用gzip压缩返回值