Compare commits

..
18 Commits
Author SHA1 Message Date
MoeFurina 60ee927e32 fix: 修复听歌打卡接口 (#167) 2026-06-19 15:11:31 +08:00
MoeFurina eb07a525eb feat: 支持显示加密方式 2026-06-18 23:46:11 +08:00
MoeFurina 6fc4231142 feat(docs-format): 增加文档格式化工具 2026-06-18 23:05:44 +08:00
MoeFurina fa69736300 revert: some changes
This reverts commit c959866436.
2026-06-18 22:52:35 +08:00
MoeFurina 73a5e9d788 Revert "feat: 增加文档格式化工具"
This reverts commit 6550c7c827.
2026-06-18 22:51:34 +08:00
MoeFurina dc6f56e8a8 Revert "fix(deploy): 修复部分启动失败 (#196)"
This reverts commit 395a80e74b.
2026-06-18 22:50:53 +08:00
MoeFurina 2c9da8507d Revert "refactor: 优化serverless入口文件"
This reverts commit 1db354464f.
2026-06-18 22:50:51 +08:00
MoeFurina 1db354464f refactor: 优化serverless入口文件 2026-06-18 22:29:24 +08:00
MoeFurina 395a80e74b fix(deploy): 修复部分启动失败 (#196) 2026-06-18 21:00:06 +08:00
MoeFurina 6550c7c827 feat: 增加文档格式化工具 2026-06-13 23:49:30 +08:00
MoeFurina c959866436 feat: 增强配置生成逻辑,添加重试机制和日志记录 2026-06-13 21:21:33 +08:00
MoeFurina e3a1c041b6 fix: 修复游客登陆接口 2026-06-09 00:09:48 +08:00
MoeFurina e2324e26da Merge remote-tracking branch 'origin/dependabot/github_actions/docker/login-action-4' 2026-06-09 00:09:09 +08:00
MoeFurina f01e770c35 Merge remote-tracking branch 'origin/dependabot/npm_and_yarn/types/node-25.9.1' 2026-06-09 00:08:41 +08:00
MoeFurina 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
15 changed files with 772 additions and 89 deletions
@@ -260,13 +260,13 @@ jobs:
uses: docker/setup-buildx-action@v3
- name: Log in to Docker Hub
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
+1 -1
View File
@@ -22,7 +22,7 @@ jobs:
# Step 2: run the sync action
- name: Sync upstream changes
id: sync
uses: aormsby/Fork-Sync-With-Upstream-action@v3.4.2
uses: aormsby/Fork-Sync-With-Upstream-action@v3.4.3
with:
upstream_sync_repo: NeteaseCloudMusicApiEnhanced/api-enhanced
upstream_sync_branch: main
+23
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.
+3 -1
View File
@@ -1,3 +1,5 @@
// 获取游客cookie
const CryptoJS = require('crypto-js')
const path = require('path')
const fs = require('fs')
@@ -37,7 +39,7 @@ module.exports = async (query, request) => {
let result = await request(
`/api/register/anonimous`,
data,
createOption(query, 'weapi'),
createOption(query, 'xeapi'),
)
if (result.body.code === 200) {
result = {
+55 -5
View File
@@ -1,8 +1,40 @@
// 听歌打卡
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {
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([
{
action: 'play',
@@ -15,12 +47,30 @@ module.exports = (query, request) => {
type: 'song',
wifi: 0,
source: 'list',
mainsite: 1,
content: '',
mainsite: '1',
mainsiteWeb: '1',
content: `id=${query.sourceid}`,
},
},
]),
}
return request(`/api/feedback/weblog`, data, createOption(query, 'weapi'))
const option = createOption(query, 'eapi')
option.domain = 'https://clientlog.music.163.com'
// 发送两次请求
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,
},
},
}
}
+5 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@neteasecloudmusicapienhanced/api",
"version": "4.35.0",
"version": "4.35.2",
"description": "全网最全的网易云音乐API接口 || A revival project for NeteaseCloudMusicApi Node.js Services (Half Refactor & Enhanced) || 网易云音乐 API 备份 + 增强 || 本项目自原版v4.28.0版本后开始自行维护",
"scripts": {
"dev": "nodemon app.js",
@@ -9,6 +9,8 @@
"lint": "eslint \"**/*.{js,ts}\"",
"lint-fix": "eslint --fix \"**/*.{js,ts}\"",
"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",
"pkglinux": "pkg . -t node18-linux-x64 -C GZip -o precompiled/app",
"pkgmacos": "pkg . -t node18-macos-x64 -C GZip -o precompiled/app"
@@ -80,7 +82,7 @@
"express": "^5.2.1",
"express-fileupload": "^1.5.2",
"gzip": "^0.1.0",
"music-metadata": "^11.12.3",
"music-metadata": "^11.13.0",
"node-forge": "^1.4.0",
"pac-proxy-agent": "^7.2.0",
"qrcode": "^1.5.4",
@@ -95,7 +97,7 @@
"@types/express": "^5.0.6",
"@types/express-fileupload": "^1.5.1",
"@types/mocha": "^10.0.10",
"@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",
+36 -30
View File
@@ -30,8 +30,8 @@ importers:
specifier: ^0.1.0
version: 0.1.0
music-metadata:
specifier: ^11.12.3
version: 11.12.3
specifier: ^11.13.0
version: 11.13.0
node-forge:
specifier: ^1.4.0
version: 1.4.0
@@ -70,8 +70,8 @@ importers:
specifier: ^10.0.10
version: 10.0.10
'@types/node':
specifier: 25.5.0
version: 25.5.0
specifier: 25.9.2
version: 25.9.2
'@typescript-eslint/eslint-plugin':
specifier: ^8.60.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)
@@ -286,8 +286,8 @@ packages:
'@types/mocha@10.0.10':
resolution: {integrity: sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==}
'@types/node@25.5.0':
resolution: {integrity: sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==}
'@types/node@25.9.2':
resolution: {integrity: sha512-G05zqtJhcDLb8uslf5EjCxXg9G1KQxiV8OS0R26IC//Eoyitzqe8z37I7cqvnZlrlSfgocQRfSn/AHBZJJFyGw==}
'@types/qs@6.15.1':
resolution: {integrity: sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==}
@@ -1636,6 +1636,10 @@ packages:
resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==}
engines: {node: '>= 0.8'}
media-typer@2.0.0:
resolution: {integrity: sha512-kOy3OxT2HH39N70UnKgu4NWDZjLOz8W/mfyvniHjRH/DrL3f2pOfvWQ4p60offbbtDAnXWp0v9LfMIqMec269Q==}
engines: {node: '>=18'}
merge-descriptors@1.0.3:
resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==}
@@ -1729,8 +1733,8 @@ packages:
multistream@4.1.0:
resolution: {integrity: sha512-J1XDiAmmNpRCBfIWJv+n0ymC4ABcf/Pl+5YvC5B/D2f/2+8PtHvCNxMPKiQcZyi922Hq69J2YOpb1pTywfifyw==}
music-metadata@11.12.3:
resolution: {integrity: sha512-n6hSTZkuD59qWgHh6IP5dtDlDZQXoxk/bcA85Jywg8Z1iFrlNgl2+GTFgjZyn52W5UgQpV42V4XqrQZZAMbZTQ==}
music-metadata@11.13.0:
resolution: {integrity: sha512-uXRaov9dfjSpQufXIU7sMxVZnh+FilCQv2mXn+K5EJ/decP3dTWrgvPYa5r6MtRbieNSCE708Da4J0u1UGfQIw==}
engines: {node: '>=18'}
napi-build-utils@1.0.2:
@@ -2149,8 +2153,8 @@ packages:
secure-json-parse@2.7.0:
resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==}
semver@7.8.2:
resolution: {integrity: sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ==}
semver@7.8.3:
resolution: {integrity: sha512-wnilbGyMxzbY7dNOl7jpKbLSjcfeweJWU5j4+u5qW+6/wuGD9KzIGOyZnQVSBM9E7DtWaaH3CyHkppYrKYoxwg==}
engines: {node: '>=10'}
hasBin: true
@@ -2493,8 +2497,8 @@ packages:
undefsafe@2.0.5:
resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==}
undici-types@7.18.2:
resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==}
undici-types@7.24.6:
resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==}
universal-deep-strict-equal@1.2.2:
resolution: {integrity: sha512-UpnFi3/IF3jZHIHTdQXTHLCqpBP3805OFFRPHgvCS7k0oob2YVXxMTjS0U0g9qJTzqFRMwEnFFSlFLqt6zwjTQ==}
@@ -2798,15 +2802,15 @@ snapshots:
'@types/body-parser@1.19.6':
dependencies:
'@types/connect': 3.4.38
'@types/node': 25.5.0
'@types/node': 25.9.2
'@types/busboy@1.5.4':
dependencies:
'@types/node': 25.5.0
'@types/node': 25.9.2
'@types/connect@3.4.38':
dependencies:
'@types/node': 25.5.0
'@types/node': 25.9.2
'@types/estree@1.0.9': {}
@@ -2817,7 +2821,7 @@ snapshots:
'@types/express-serve-static-core@5.1.1':
dependencies:
'@types/node': 25.5.0
'@types/node': 25.9.2
'@types/qs': 6.15.1
'@types/range-parser': 1.2.7
'@types/send': 1.2.1
@@ -2834,9 +2838,9 @@ snapshots:
'@types/mocha@10.0.10': {}
'@types/node@25.5.0':
'@types/node@25.9.2':
dependencies:
undici-types: 7.18.2
undici-types: 7.24.6
'@types/qs@6.15.1': {}
@@ -2844,12 +2848,12 @@ snapshots:
'@types/send@1.2.1':
dependencies:
'@types/node': 25.5.0
'@types/node': 25.9.2
'@types/serve-static@2.2.0':
dependencies:
'@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)':
dependencies:
@@ -2919,7 +2923,7 @@ snapshots:
'@typescript-eslint/visitor-keys': 8.60.1
debug: 4.4.3
minimatch: 10.2.5
semver: 7.8.2
semver: 7.8.3
tinyglobby: 0.2.17
ts-api-utils: 2.5.0(typescript@5.9.3)
typescript: 5.9.3
@@ -4428,6 +4432,8 @@ snapshots:
media-typer@1.1.0: {}
media-typer@2.0.0: {}
merge-descriptors@1.0.3: {}
merge-descriptors@2.0.0: {}
@@ -4520,14 +4526,14 @@ snapshots:
once: 1.4.0
readable-stream: 3.6.2
music-metadata@11.12.3:
music-metadata@11.13.0:
dependencies:
'@borewit/text-codec': 0.2.2
'@tokenizer/token': 0.3.0
content-type: 1.0.5
content-type: 2.0.0
debug: 4.4.3
file-type: 21.3.4
media-typer: 1.1.0
media-typer: 2.0.0
strtok3: 10.3.5
token-types: 6.1.2
uint8array-extras: 1.5.0
@@ -4549,7 +4555,7 @@ snapshots:
node-abi@3.92.0:
dependencies:
semver: 7.8.2
semver: 7.8.3
node-fetch@2.7.0:
dependencies:
@@ -4569,7 +4575,7 @@ snapshots:
ignore-by-default: 1.0.1
minimatch: 10.2.5
pstree.remy: 1.1.8
semver: 7.8.2
semver: 7.8.3
simple-update-notifier: 2.0.0
supports-color: 5.5.0
touch: 3.1.1
@@ -4744,7 +4750,7 @@ snapshots:
https-proxy-agent: 5.0.1
node-fetch: 2.7.0
progress: 2.0.3
semver: 7.8.2
semver: 7.8.3
tar-fs: 2.1.4
yargs: 16.2.0
transitivePeerDependencies:
@@ -5038,7 +5044,7 @@ snapshots:
secure-json-parse@2.7.0: {}
semver@7.8.2: {}
semver@7.8.3: {}
send@0.19.2:
dependencies:
@@ -5168,7 +5174,7 @@ snapshots:
simple-update-notifier@2.0.0:
dependencies:
semver: 7.8.2
semver: 7.8.3
slash@3.0.0: {}
@@ -5471,7 +5477,7 @@ snapshots:
undefsafe@2.0.5: {}
undici-types@7.18.2: {}
undici-types@7.24.6: {}
universal-deep-strict-equal@1.2.2:
dependencies:
+1 -1
View File
@@ -11,4 +11,4 @@
[前往本家](https://github.com/binaryify/NeteaseCloudMusicApi)
[快速开始](#neteasecloudmusicapienhanced)
![color](#ffffff)
![color](#ffffff)
+38 -26
View File
@@ -88,6 +88,7 @@ v4.29.9 加入了生成随机中国 IP 功能, 在请求时加上 `randomCNIP=tr
5. 直接点`Continue`
6. `PROJECT NAME`自己填,`FRAMEWORK PRESET` 选 `Other` 然后直接点 `Deploy` 接着等部署完成即可
## 腾讯云 serverless 部署
因 `Vercel` 在国内访问太慢(不绑定自己的域名的情况下),在此提供腾讯云 serverless 部署方法
@@ -101,6 +102,7 @@ v4.29.9 加入了生成随机中国 IP 功能, 在请求时加上 `randomCNIP=tr
5. 输入`应用名`,上传方式选择`代码仓库`,进行 GitHub 授权(如已授权可跳过这一步),代码仓库选择刚刚 fork 的项目
6. 启动文件填入:
```
#!/bin/bash
export PORT=9000
@@ -113,6 +115,7 @@ export PORT=9000
- 腾讯云 serverless 并不是免费的,前三个月有免费额度,之后收费
- 当前(2024-08-24), 用此法创建的话, 会`默认`关联一个"日志服务-日志主题"(创建过程中没有提醒), 此服务是计量收费的
## 可以使用代理
在 query 参数中加上 proxy=your-proxy 即可让这一次的请求使用 proxy
@@ -180,6 +183,7 @@ request 相关的环境变量
5. no_proxy
6. NO_PROXY
```shell
docker pull moefurina/ncm-api
@@ -214,6 +218,7 @@ $ sudo docker run -d -p 3000:3000 netease-music-api
- 需要返回值加密时, 可传 `e_r=1`, `weapi` 和 `eapi` 都支持
- 目前支持算法 有 `weapi`, `eapi`, `linuxapi` 和 `xeapi` (xeapi 是一种不加密的特殊算法, 主要用于调试加密前的原始请求参数)
## 接口文档
### 调用前须知
@@ -268,12 +273,12 @@ AI 生成的图,仅供娱乐()
#### 1. 手机登录
**必选参数 :**
**必选参数 :**
`phone`: 手机号码
`password`: 密码
**可选参数 :**
**可选参数 :**
`countrycode`: 国家码,用于国外手机号登录,例如美国传入:`1`
`md5_password`: md5 加密后的密码,传入后 `password` 参数将失效
@@ -432,11 +437,11 @@ body {
### 检测手机号码是否已注册
说明 : 调用此接口 ,可检测手机号码是否已注册
**必选参数 :**
说明 : 调用此接口 ,可检测手机号码是否已注册
**必选参数 :**
`phone` : 手机号码
**可选参数 :**
**可选参数 :**
`countrycode`: 国家码,用于国外手机号,例如美国传入:`1` ,默认 86 即中国
**接口地址 :** `/cellphone/existence/check`
@@ -445,7 +450,7 @@ body {
### 初始化昵称
说明 : 刚注册的账号(需登录),调用此接口 ,可初始化昵称
说明 : 刚注册的账号(需登录),调用此接口 ,可初始化昵称
**必选参数 :**
`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)
**必选参数 :**
**必选参数 :**
`id`: 歌单 id 3143833470
**可选参数 :**
@@ -730,7 +735,7 @@ tags: 歌单标签
说明 : 登录后调用此接口,可以根据歌曲 id 顺序调整歌曲顺序
**必选参数 :**
**必选参数 :**
`pid`: 歌单 id
`ids`: 歌曲 id 列表
@@ -1210,6 +1215,7 @@ tags: 歌单标签
> 如果你设置 limit=50&offset=100,你就会得到第 101-150 首歌曲
### 歌单详情动态
说明 : 调用后可获取歌单详情动态部分,如评论数,是否收藏,播放数
@@ -1403,7 +1409,7 @@ tags: 歌单标签
### 歌单收藏者
说明 : 调用此接口 , 传入歌单 id 可获取歌单的所有收藏者
说明 : 调用此接口 , 传入歌单 id 可获取歌单的所有收藏者
**必选参数 :**
`id` : 歌单 id
@@ -1496,6 +1502,7 @@ tags: 歌单标签
- (可能存在)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":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)没...
~~~~1 ~~~2 ~~~~3 ~~4 5 ~6 (...)
@@ -1588,7 +1596,7 @@ tags: 歌单标签
说明 : 调用此接口 , 传入资源 parentCommentId 和资源类型 type 和资源 id 参数, 可获得该资源的歌曲楼层评论
**必选参数 :**
**必选参数 :**
`parentCommentId`: 楼层评论 id
`id` : 资源 id
@@ -1804,7 +1812,7 @@ tags: 歌单标签
说明 : 调用此接口 , 传入资源类型和资源 id,以及排序方式,可获取对应资源的评论
**必选参数 :**
**必选参数 :**
`id` : 资源 id, 如歌曲 id,mv id
`type`: 数字 , 资源类型 , 对应歌曲 , mv, 专辑 , 歌单 , 电台, 视频对应以下类型
@@ -1827,7 +1835,7 @@ tags: 歌单标签
7: 电台
```
**可选参数 :**
**可选参数 :**
`pageNo`:分页参数,第 N 页,默认为 1
`pageSize`:分页参数,每页多少条数据,默认 20
@@ -2200,7 +2208,7 @@ privilege:权限相关信息
说明 : 调用此接口 , 可获得已收藏专辑列表
**可选参数 :**
**可选参数 :**
`limit`: 取出数量 , 默认为 25
`offset`: 偏移数量 , 用于分页 , 如 :( 页数 -1)\*25, 其中 25 为 limit 的值 , 默认
@@ -2521,7 +2529,7 @@ privilege:权限相关信息
说明 : 调用此接口 , 可获取全部 mv
**可选参数 :**
**可选参数 :**
`area`: 地区,可选值为全部,内地,港台,欧美,日本,韩国,不填则为全部
`type`: 类型,可选值为全部,官方版,原生,现场版,网易出品,不填则为全部
@@ -2603,7 +2611,7 @@ privilege:权限相关信息
**接口地址 :** `/program/recommend`
**可选参数 :**
**可选参数 :**
`limit`: 取出数量 , 默认为 10
`offset`: 偏移数量 , 用于分页 , 如 :( 页数 -1)\*10, 其中 10 为 limit 的值 , 默认
@@ -2885,6 +2893,7 @@ type : 地区
- 适合 Vercel、Netlify 等有请求体限制的平台
- 需要前端配合实现
#### 客户端直传相关接口
**获取上传凭证**
@@ -2931,6 +2940,7 @@ type : 地区
- `artist`: 艺术家
- `album`: 专辑名
#### 客户端直传流程
1. 客户端计算文件 MD5
@@ -2938,11 +2948,12 @@ type : 地区
3. 如果 `needUpload` 为 true,直接 PUT 文件到 `uploadUrl`
4. 调用 `/cloud/upload/complete` 完成导入
### 云盘歌曲信息匹配纠正
说明 : 登录后调用此接口,可对云盘歌曲信息匹配纠正,如需取消匹配,asid 需要传 0
**必选参数 :**
**必选参数 :**
`uid`: 用户 id
`sid`: 云盘的歌曲 id
@@ -3413,7 +3424,7 @@ type='1009' 获取其 id, 如`/search?keywords= 代码时间 &type=1009`
`limit` : 返回数量 , 默认为 30
`offset` : 偏移数量,用于分页 , 如 :( 页数 -1)\*30, 其中 30 为 limit 的值 , 默认为 0
`offset` : 偏移数量,用于分页 , 如 :( 页数 -1)\*30, 其中 30 为 limit 的值 , 默认为 0
**接口地址 :** `/album/list`
**调用例子 :** `/album/list?limit=10`
@@ -3572,7 +3583,7 @@ type='1009' 获取其 id, 如`/search?keywords= 代码时间 &type=1009`
**可选参数 :** `limit`: 取出评论数量 , 默认为 10
`offset`: 偏移数量 , 用于分页 , 如 :( 评论页数 -1)\*10, 其中 10 为 limit 的值
`offset`: 偏移数量 , 用于分页 , 如 :( 评论页数 -1)\*10, 其中 10 为 limit 的值
**接口地址 :** `/yunbei/tasks/expense`
**调用例子 :** `/yunbei/tasks/expense?limit=1`
@@ -4210,6 +4221,7 @@ ONLINE 已发布
- `voiceFeeType: 0`:返回免费的声音
- `voiceFeeType: 1`:返回收费的声音
### 播客声音详情
说明: 获取播客里的声音详情
@@ -5131,7 +5143,7 @@ let data = encodeURIComponent(
说明 : 登录后调用此接口, 获取我创建的博客声音
**可选参数 :**
**可选参数 :**
`limit` : 返回数量 , 默认为 20
@@ -5144,7 +5156,7 @@ let data = encodeURIComponent(
说明: 调用此接口, 获取DIFM电台分类
**必选参数 :**
**必选参数 :**
`sources`: 来源列表, 0: 最嗨电音 1: 古典电台 2: 爵士电台
@@ -5156,7 +5168,7 @@ let data = encodeURIComponent(
说明: 调用此接口, 获取DIFM电台收藏列表
**必选参数 :**
**必选参数 :**
`sources`: 来源列表, 0: 最嗨电音 1: 古典电台 2: 爵士电台
@@ -5192,7 +5204,7 @@ let data = encodeURIComponent(
说明: 调用此接口, 获取DIFM播放列表
**必选参数 :**
**必选参数 :**
`source`: 来源, 0: 最嗨电音 1: 古典电台 2: 爵士电台
@@ -5226,7 +5238,7 @@ let data = encodeURIComponent(
说明: 调用此接口, 获取标签下资源列表; 接口返回的`trackId`可以用于请求`/song/url/v1`接口,用于获取声音的下载地址
**必选参数 :**
**必选参数 :**
`tag`: 标签, 由标签列表接口得到
@@ -5238,7 +5250,7 @@ let data = encodeURIComponent(
说明: 调用此接口, 查看同类推荐
**必选参数 :**
**必选参数 :**
`id`: id, `/sati/tag/list`接口返回的`trackId`
@@ -5258,7 +5270,7 @@ let data = encodeURIComponent(
说明: 调用此接口, 收藏声音
**必选参数 :**
**必选参数 :**
`id`: id, `/sati/tag/list`接口返回的`trackId`
@@ -5274,7 +5286,7 @@ let data = encodeURIComponent(
说明: 调用此接口,获取跑步漫游的歌曲信息
**必选参数:**
**必选参数:**
`bpm`: 步频
+3 -3
View File
@@ -148,12 +148,12 @@
updateStatus('二维码已过期,请刷新页面', 'error')
clearInterval(timer)
} else if (statusRes.code === 801) {
updateStatus('二维码已扫描,请在手机上确认', 'waiting')
updateStatus('等待手机扫码...', 'waiting')
} else if (statusRes.code === 802) {
updateStatus('登录成功,正在保存信息...', 'waiting')
updateStatus('二维码已扫描,请在手机上确认', 'waiting')
} else if (statusRes.code === 803) {
clearInterval(timer)
updateStatus('授权登录成功!', 'success')
updateStatus('授权登录成功,正在保存信息...', 'success')
await getLoginStatus(statusRes.cookie)
localStorage.setItem('cookie', statusRes.cookie)
}
+3 -3
View File
@@ -148,12 +148,12 @@
updateStatus('二维码已过期,请刷新页面', 'error')
clearInterval(timer)
} else if (statusRes.code === 801) {
updateStatus('二维码已扫描,请在手机上确认', 'waiting')
updateStatus('等待手机扫码...', 'waiting')
} else if (statusRes.code === 802) {
updateStatus('登录成功,正在保存信息...', 'waiting')
updateStatus('二维码已扫描,请在手机上确认', 'waiting')
} else if (statusRes.code === 803) {
clearInterval(timer)
updateStatus('授权登录成功!', 'success')
updateStatus('授权登录成功,正在保存信息...', 'success')
await getLoginStatus(statusRes.cookie)
localStorage.setItem('cookie', statusRes.cookie)
}
+577
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 }
+11 -10
View File
@@ -10,6 +10,7 @@ const { cookieToJson } = require('./util/index')
const fileUpload = require('express-fileupload')
const decode = require('safe-decode-uri-component')
const logger = require('./util/logger.js')
const { APP_CONF } = require('./util/config.json')
/**
* The version check result.
@@ -299,15 +300,15 @@ async function constructServer(moduleDefs) {
)
try {
let usedCrypto = ''
const moduleResponse = await moduleDef.module(query, (...params) => {
// 参数注入客户端IP
const obj = [...params]
const options = obj[2] || {}
usedCrypto = options.crypto || ''
let ip = ''
if (options.randomCNIP) {
ip = global.cnIp
// logger.info('Using random Chinese IP for request:', ip)
} else {
ip = req.ip
@@ -317,7 +318,6 @@ async function constructServer(moduleDefs) {
if (ip == '::1') {
ip = global.cnIp
}
// logger.info('Requested from ip:', ip)
}
obj[2] = {
@@ -327,7 +327,10 @@ async function constructServer(moduleDefs) {
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
if (
@@ -422,7 +425,7 @@ async function serveNcmApi(options) {
options.checkVersion &&
checkVersion().then(({ npmVersion, ourVersion, status }) => {
if (status == VERSION_CHECK_RESULT.NOT_LATEST) {
logger.info(
logger.warn(
`最新版本: ${npmVersion}, 当前版本: ${ourVersion}, 请及时更新`,
)
}
@@ -444,11 +447,9 @@ async function serveNcmApi(options) {
╠═╣╠═╝║ ║╣ ║║║╠═╣╠═╣║║║║ ║╣ ║║
╩ ╩╩ ╩ ╚═╝╝╚╝╩ ╩╩ ╩╝╚╝╚═╝╚═╝═╩╝
`)
logger.info(`
- Server started successfully @ http://${host ? host : 'localhost'}:${port}
- Environment: ${process.env.NODE_ENV || 'development'}
- Node Version: ${process.version}
- Process ID: ${process.pid}`)
logger.info(
`Server started successfully @ http://${host ? host : 'localhost'}:${port}`,
)
})
return appExt
+3 -3
View File
@@ -62,9 +62,9 @@ const chinaIPRanges = (function loadChinaIPRanges() {
// attach total for convenience
arr.totalCount = total
logger.info(
`Loaded ${arr.length} Chinese IP ranges from china_ip_ranges.txt, total ${total} IPs`,
)
// logger.info(
// `Loaded ${arr.length} Chinese IP ranges from china_ip_ranges.txt, total ${total} IPs`,
// )
return arr
} catch (error) {
logger.error('Failed to load china_ip_ranges.txt:', error.message)
+11 -1
View File
@@ -88,6 +88,12 @@ const osMap = {
osver: '16.2',
channel: 'distribution',
},
osx: {
os: 'osx',
appver: '3.1.10.5100',
osver: '15.5',
channel: 'netease',
},
}
// 预先定义userAgentMap
@@ -306,7 +312,11 @@ const createRequest = (uri, data, options) => {
if (cookie.MUSIC_A) header['MUSIC_A'] = cookie.MUSIC_A
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') {
// headers['x-aeapi'] = true // 服务器会使用gzip压缩返回值