Compare commits

..

6 Commits

Author SHA1 Message Date
dependabot[bot]
b8ac9b60fb
build(packages): bump lint-staged from 16.4.0 to 17.0.7
Bumps [lint-staged](https://github.com/lint-staged/lint-staged) from 16.4.0 to 17.0.7.
- [Release notes](https://github.com/lint-staged/lint-staged/releases)
- [Changelog](https://github.com/lint-staged/lint-staged/blob/main/CHANGELOG.md)
- [Commits](https://github.com/lint-staged/lint-staged/compare/v16.4.0...v17.0.7)

---
updated-dependencies:
- dependency-name: lint-staged
  dependency-version: 17.0.7
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-18 14:55:20 +00: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
12 changed files with 81 additions and 798 deletions

13
app.js
View File

@ -8,16 +8,11 @@ async function start() {
if (!fs.existsSync(path.resolve(tmpPath, 'anonymous_token'))) { if (!fs.existsSync(path.resolve(tmpPath, 'anonymous_token'))) {
fs.writeFileSync(path.resolve(tmpPath, 'anonymous_token'), '', 'utf-8') fs.writeFileSync(path.resolve(tmpPath, 'anonymous_token'), '', 'utf-8')
} }
// 启动时更新anonymous_tokenVercel 构建环境下跳过网络请求喵~ // 启动时更新anonymous_token
if (!process.env.VERCEL_ENV) { const generateConfig = require('./generateConfig')
const generateConfig = require('./generateConfig') await generateConfig()
await generateConfig()
}
require('./server').serveNcmApi({ require('./server').serveNcmApi({
checkVersion: true, checkVersion: true,
}) })
} }
start().catch((err) => { start()
console.error('[FATAL] 启动失败:', err)
process.exit(1)
})

View File

@ -4,159 +4,38 @@ const { register_anonimous } = require('./main')
const { cookieToJson, generateRandomChineseIP } = require('./util/index') const { cookieToJson, generateRandomChineseIP } = require('./util/index')
const { getXeapiPublicKey } = require('./util/xeapiKey') const { getXeapiPublicKey } = require('./util/xeapiKey')
const tmpPath = require('os').tmpdir() const tmpPath = require('os').tmpdir()
const logger = require('./util/logger')
const MAX_RETRIES = 10
const RETRY_DELAY_MS = 1000
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms))
}
function isRetryableError(err) {
const status =
(err && err.status) || (err && err.response && err.response.status)
if (status && status >= 500) {
return true
}
return false
}
/** @returns {{ success: boolean, error?: Error }} */
async function fetchAnonymousToken() {
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
try {
const res = await register_anonimous()
const body = res.body
if (body.code && body.code !== 200) {
const err = new Error(`匿名注册返回错误码 ${body.code}`)
err.status = 502
throw err
}
const cookie = body.cookie
if (cookie) {
const cookieObj = cookieToJson(cookie)
fs.writeFileSync(
path.resolve(tmpPath, 'anonymous_token'),
cookieObj.MUSIC_A,
'utf-8',
)
logger.success('[generateConfig] 匿名 token 注册成功')
return { success: true }
}
logger.warn(
`[generateConfig] 匿名注册返回了空 cookie (attempt ${attempt})`,
)
return {
success: false,
error: new Error('empty cookie from anonymous register'),
}
} catch (err) {
if (isRetryableError(err) && attempt < MAX_RETRIES) {
const delay = RETRY_DELAY_MS * Math.pow(2, attempt - 1)
logger.warn(
`[generateConfig] 获取匿名 token 失败 (attempt ${attempt}/${MAX_RETRIES}), ${delay}ms 后重试...`,
)
await sleep(delay)
continue
}
if (attempt >= MAX_RETRIES) {
logger.error(
`[generateConfig] 获取匿名 token 已达最大重试次数 (${MAX_RETRIES}):`,
err.message,
)
} else {
logger.error(
`[generateConfig] 获取匿名 token 失败 (不可重试):`,
err.message,
)
}
return { success: false, error: err }
}
}
return { success: false, error: new Error('unreachable') }
}
/**
* 获取 xeapi public key带重试
* @returns {{ success: boolean, error?: Error }}
*/
async function fetchXeapiPublicKey() {
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
try {
let currentPublicKey = {}
try {
currentPublicKey = JSON.parse(
fs.readFileSync(path.resolve(tmpPath, 'xeapi_public_key'), 'utf-8'),
)
} catch (_) {
// 本地无缓存文件,用空对象正常请求
}
const publicKey = await getXeapiPublicKey(
currentPublicKey,
global.deviceId,
)
fs.writeFileSync(
path.resolve(tmpPath, 'xeapi_public_key'),
JSON.stringify(publicKey),
'utf-8',
)
logger.success('[generateConfig] xeapi public key 获取成功')
return { success: true }
} catch (err) {
if (isRetryableError(err) && attempt < MAX_RETRIES) {
const delay = RETRY_DELAY_MS * Math.pow(2, attempt - 1)
logger.warn(
`[generateConfig] 获取 xeapi public key 失败 (attempt ${attempt}/${MAX_RETRIES}), ${delay}ms 后重试...`,
)
await sleep(delay)
continue
}
if (attempt >= MAX_RETRIES) {
logger.error(
`[generateConfig] 获取 xeapi public key 已达最大重试次数 (${MAX_RETRIES}):`,
err.message,
)
} else {
logger.error(
`[generateConfig] 获取 xeapi public key 失败 (不可重试):`,
err.message,
)
}
return { success: false, error: err }
}
}
return { success: false, error: new Error('unreachable') }
}
/**
* 生成配置匿名 token + xeapi public key带容错重试
* @returns {{ tokenOk: boolean, keyOk: boolean }}
*/
async function generateConfig() { async function generateConfig() {
global.cnIp = generateRandomChineseIP() global.cnIp = generateRandomChineseIP()
try {
// 两个任务并行执行,互不影响喵~ const res = await register_anonimous()
const [tokenResult, keyResult] = await Promise.all([ const cookie = res.body.cookie
fetchAnonymousToken(), if (cookie) {
fetchXeapiPublicKey(), const cookieObj = cookieToJson(cookie)
]) fs.writeFileSync(
path.resolve(tmpPath, 'anonymous_token'),
if (!tokenResult.success) { cookieObj.MUSIC_A,
logger.warn('[generateConfig] 匿名 token 获取失败') 'utf-8',
)
}
} catch (error) {
console.log(error)
} }
if (!keyResult.success) { try {
logger.warn('[generateConfig] xeapi public key 获取失败') let currentPublicKey = {}
} try {
currentPublicKey = JSON.parse(
if (tokenResult.success && keyResult.success) { fs.readFileSync(path.resolve(tmpPath, 'xeapi_public_key'), 'utf-8'),
logger.success('[generateConfig] 配置初始化完成') )
} } catch (_) {}
const publicKey = await getXeapiPublicKey(currentPublicKey, global.deviceId)
return { fs.writeFileSync(
tokenOk: tokenResult.success, path.resolve(tmpPath, 'xeapi_public_key'),
keyOk: keyResult.success, JSON.stringify(publicKey),
'utf-8',
)
} catch (error) {
console.log(error)
} }
} }
module.exports = generateConfig module.exports = generateConfig

1
index.mjs Normal file
View File

@ -0,0 +1 @@
import './app.js'

View File

@ -26,6 +26,7 @@ function cloudmusic_dll_encode_id(some_id) {
module.exports = async (query, request) => { module.exports = async (query, request) => {
const deviceId = generateDeviceId() const deviceId = generateDeviceId()
logger.info(`Successfully registered anonimous token, deviceId: ${deviceId}`)
global.deviceId = deviceId global.deviceId = deviceId
const encodedId = CryptoJS.enc.Base64.stringify( const encodedId = CryptoJS.enc.Base64.stringify(
CryptoJS.enc.Utf8.parse( CryptoJS.enc.Utf8.parse(

View File

@ -9,10 +9,6 @@
"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",
"docs:toc": "node scripts/format-docs.js --toc",
"docs:all": "node scripts/format-docs.js --dir public/docs --toc --verbose",
"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"
@ -54,7 +50,7 @@
}, },
"homepage": "https://neteasecloudmusicapienhanced.js.org/", "homepage": "https://neteasecloudmusicapienhanced.js.org/",
"engines": { "engines": {
"node": ">=22" "node": ">=12"
}, },
"lint-staged": { "lint-staged": {
"*.js": [ "*.js": [

View File

@ -1,9 +1,3 @@
## 目录
- [NeteaseCloudMusicAPI Enhanced](#neteasecloudmusicapi-enhanced)
---
# NeteaseCloudMusicAPI Enhanced # NeteaseCloudMusicAPI Enhanced
> 🎉 全网收集最全的网易云音乐api接口 基于[NeteaseCloudMusicAPI](https://github.com/binaryify/NeteaseCloudMusicApi)的复刻版本 > 🎉 全网收集最全的网易云音乐api接口 基于[NeteaseCloudMusicAPI](https://github.com/binaryify/NeteaseCloudMusicApi)的复刻版本
@ -17,4 +11,4 @@
[前往本家](https://github.com/binaryify/NeteaseCloudMusicApi) [前往本家](https://github.com/binaryify/NeteaseCloudMusicApi)
[快速开始](#neteasecloudmusicapienhanced) [快速开始](#neteasecloudmusicapienhanced)
![color](#ffffff) ![color](#ffffff)

View File

@ -88,7 +88,6 @@ 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 部署方法
@ -102,7 +101,6 @@ v4.29.9 加入了生成随机中国 IP 功能, 在请求时加上 `randomCNIP=tr
5. 输入`应用名`,上传方式选择`代码仓库`,进行 GitHub 授权(如已授权可跳过这一步),代码仓库选择刚刚 fork 的项目 5. 输入`应用名`,上传方式选择`代码仓库`,进行 GitHub 授权(如已授权可跳过这一步),代码仓库选择刚刚 fork 的项目
6. 启动文件填入: 6. 启动文件填入:
``` ```
#!/bin/bash #!/bin/bash
export PORT=9000 export PORT=9000
@ -115,7 +113,6 @@ export PORT=9000
- 腾讯云 serverless 并不是免费的,前三个月有免费额度,之后收费 - 腾讯云 serverless 并不是免费的,前三个月有免费额度,之后收费
- 当前(2024-08-24), 用此法创建的话, 会`默认`关联一个"日志服务-日志主题"(创建过程中没有提醒), 此服务是计量收费的 - 当前(2024-08-24), 用此法创建的话, 会`默认`关联一个"日志服务-日志主题"(创建过程中没有提醒), 此服务是计量收费的
## 可以使用代理 ## 可以使用代理
在 query 参数中加上 proxy=your-proxy 即可让这一次的请求使用 proxy 在 query 参数中加上 proxy=your-proxy 即可让这一次的请求使用 proxy
@ -183,7 +180,6 @@ 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
@ -218,7 +214,6 @@ $ 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 是一种不加密的特殊算法, 主要用于调试加密前的原始请求参数)
## 接口文档 ## 接口文档
### 调用前须知 ### 调用前须知
@ -273,12 +268,12 @@ AI 生成的图,仅供娱乐()
#### 1. 手机登录 #### 1. 手机登录
**必选参数 :** **必选参数 :**
`phone`: 手机号码 `phone`: 手机号码
`password`: 密码 `password`: 密码
**可选参数 :** **可选参数 :**
`countrycode`: 国家码,用于国外手机号登录,例如美国传入:`1` `countrycode`: 国家码,用于国外手机号登录,例如美国传入:`1`
`md5_password`: md5 加密后的密码,传入后 `password` 参数将失效 `md5_password`: md5 加密后的密码,传入后 `password` 参数将失效
@ -437,11 +432,11 @@ body {
### 检测手机号码是否已注册 ### 检测手机号码是否已注册
说明 : 调用此接口 ,可检测手机号码是否已注册 说明 : 调用此接口 ,可检测手机号码是否已注册
**必选参数 :** **必选参数 :**
`phone` : 手机号码 `phone` : 手机号码
**可选参数 :** **可选参数 :**
`countrycode`: 国家码,用于国外手机号,例如美国传入:`1` ,默认 86 即中国 `countrycode`: 国家码,用于国外手机号,例如美国传入:`1` ,默认 86 即中国
**接口地址 :** `/cellphone/existence/check` **接口地址 :** `/cellphone/existence/check`
@ -450,7 +445,7 @@ body {
### 初始化昵称 ### 初始化昵称
说明 : 刚注册的账号(需登录),调用此接口 ,可初始化昵称 说明 : 刚注册的账号(需登录),调用此接口 ,可初始化昵称
**必选参数 :** **必选参数 :**
`nickname` : 昵称 `nickname` : 昵称
@ -705,7 +700,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
**可选参数 :** **可选参数 :**
@ -735,7 +730,7 @@ tags: 歌单标签
说明 : 登录后调用此接口,可以根据歌曲 id 顺序调整歌曲顺序 说明 : 登录后调用此接口,可以根据歌曲 id 顺序调整歌曲顺序
**必选参数 :** **必选参数 :**
`pid`: 歌单 id `pid`: 歌单 id
`ids`: 歌曲 id 列表 `ids`: 歌曲 id 列表
@ -1215,7 +1210,6 @@ tags: 歌单标签
> 如果你设置 limit=50&offset=100你就会得到第 101-150 首歌曲 > 如果你设置 limit=50&offset=100你就会得到第 101-150 首歌曲
### 歌单详情动态 ### 歌单详情动态
说明 : 调用后可获取歌单详情动态部分,如评论数,是否收藏,播放数 说明 : 调用后可获取歌单详情动态部分,如评论数,是否收藏,播放数
@ -1409,7 +1403,7 @@ tags: 歌单标签
### 歌单收藏者 ### 歌单收藏者
说明 : 调用此接口 , 传入歌单 id 可获取歌单的所有收藏者 说明 : 调用此接口 , 传入歌单 id 可获取歌单的所有收藏者
**必选参数 :** **必选参数 :**
`id` : 歌单 id `id` : 歌单 id
@ -1502,7 +1496,6 @@ 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"}]}
@ -1519,7 +1512,6 @@ 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 (...)
@ -1596,7 +1588,7 @@ tags: 歌单标签
说明 : 调用此接口 , 传入资源 parentCommentId 和资源类型 type 和资源 id 参数, 可获得该资源的歌曲楼层评论 说明 : 调用此接口 , 传入资源 parentCommentId 和资源类型 type 和资源 id 参数, 可获得该资源的歌曲楼层评论
**必选参数 :** **必选参数 :**
`parentCommentId`: 楼层评论 id `parentCommentId`: 楼层评论 id
`id` : 资源 id `id` : 资源 id
@ -1812,7 +1804,7 @@ tags: 歌单标签
说明 : 调用此接口 , 传入资源类型和资源 id,以及排序方式,可获取对应资源的评论 说明 : 调用此接口 , 传入资源类型和资源 id,以及排序方式,可获取对应资源的评论
**必选参数 :** **必选参数 :**
`id` : 资源 id, 如歌曲 id,mv id `id` : 资源 id, 如歌曲 id,mv id
`type`: 数字 , 资源类型 , 对应歌曲 , mv, 专辑 , 歌单 , 电台, 视频对应以下类型 `type`: 数字 , 资源类型 , 对应歌曲 , mv, 专辑 , 歌单 , 电台, 视频对应以下类型
@ -1835,7 +1827,7 @@ tags: 歌单标签
7: 电台 7: 电台
``` ```
**可选参数 :** **可选参数 :**
`pageNo`:分页参数,第 N 页,默认为 1 `pageNo`:分页参数,第 N 页,默认为 1
`pageSize`:分页参数,每页多少条数据,默认 20 `pageSize`:分页参数,每页多少条数据,默认 20
@ -2208,7 +2200,7 @@ privilege:权限相关信息
说明 : 调用此接口 , 可获得已收藏专辑列表 说明 : 调用此接口 , 可获得已收藏专辑列表
**可选参数 :** **可选参数 :**
`limit`: 取出数量 , 默认为 25 `limit`: 取出数量 , 默认为 25
`offset`: 偏移数量 , 用于分页 , 如 :( 页数 -1)\*25, 其中 25 为 limit 的值 , 默认 `offset`: 偏移数量 , 用于分页 , 如 :( 页数 -1)\*25, 其中 25 为 limit 的值 , 默认
@ -2529,7 +2521,7 @@ privilege:权限相关信息
说明 : 调用此接口 , 可获取全部 mv 说明 : 调用此接口 , 可获取全部 mv
**可选参数 :** **可选参数 :**
`area`: 地区,可选值为全部,内地,港台,欧美,日本,韩国,不填则为全部 `area`: 地区,可选值为全部,内地,港台,欧美,日本,韩国,不填则为全部
`type`: 类型,可选值为全部,官方版,原生,现场版,网易出品,不填则为全部 `type`: 类型,可选值为全部,官方版,原生,现场版,网易出品,不填则为全部
@ -2611,7 +2603,7 @@ privilege:权限相关信息
**接口地址 :** `/program/recommend` **接口地址 :** `/program/recommend`
**可选参数 :** **可选参数 :**
`limit`: 取出数量 , 默认为 10 `limit`: 取出数量 , 默认为 10
`offset`: 偏移数量 , 用于分页 , 如 :( 页数 -1)\*10, 其中 10 为 limit 的值 , 默认 `offset`: 偏移数量 , 用于分页 , 如 :( 页数 -1)\*10, 其中 10 为 limit 的值 , 默认
@ -2893,7 +2885,6 @@ type : 地区
- 适合 Vercel、Netlify 等有请求体限制的平台 - 适合 Vercel、Netlify 等有请求体限制的平台
- 需要前端配合实现 - 需要前端配合实现
#### 客户端直传相关接口 #### 客户端直传相关接口
**获取上传凭证** **获取上传凭证**
@ -2940,7 +2931,6 @@ type : 地区
- `artist`: 艺术家 - `artist`: 艺术家
- `album`: 专辑名 - `album`: 专辑名
#### 客户端直传流程 #### 客户端直传流程
1. 客户端计算文件 MD5 1. 客户端计算文件 MD5
@ -2948,12 +2938,11 @@ 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
@ -3424,7 +3413,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`
@ -3583,7 +3572,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`
@ -4221,7 +4210,6 @@ ONLINE 已发布
- `voiceFeeType: 0`:返回免费的声音 - `voiceFeeType: 0`:返回免费的声音
- `voiceFeeType: 1`:返回收费的声音 - `voiceFeeType: 1`:返回收费的声音
### 播客声音详情 ### 播客声音详情
说明: 获取播客里的声音详情 说明: 获取播客里的声音详情
@ -5143,7 +5131,7 @@ let data = encodeURIComponent(
说明 : 登录后调用此接口, 获取我创建的博客声音 说明 : 登录后调用此接口, 获取我创建的博客声音
**可选参数 :** **可选参数 :**
`limit` : 返回数量 , 默认为 20 `limit` : 返回数量 , 默认为 20
@ -5156,7 +5144,7 @@ let data = encodeURIComponent(
说明: 调用此接口, 获取DIFM电台分类 说明: 调用此接口, 获取DIFM电台分类
**必选参数 :** **必选参数 :**
`sources`: 来源列表, 0: 最嗨电音 1: 古典电台 2: 爵士电台 `sources`: 来源列表, 0: 最嗨电音 1: 古典电台 2: 爵士电台
@ -5168,7 +5156,7 @@ let data = encodeURIComponent(
说明: 调用此接口, 获取DIFM电台收藏列表 说明: 调用此接口, 获取DIFM电台收藏列表
**必选参数 :** **必选参数 :**
`sources`: 来源列表, 0: 最嗨电音 1: 古典电台 2: 爵士电台 `sources`: 来源列表, 0: 最嗨电音 1: 古典电台 2: 爵士电台
@ -5204,7 +5192,7 @@ let data = encodeURIComponent(
说明: 调用此接口, 获取DIFM播放列表 说明: 调用此接口, 获取DIFM播放列表
**必选参数 :** **必选参数 :**
`source`: 来源, 0: 最嗨电音 1: 古典电台 2: 爵士电台 `source`: 来源, 0: 最嗨电音 1: 古典电台 2: 爵士电台
@ -5238,7 +5226,7 @@ let data = encodeURIComponent(
说明: 调用此接口, 获取标签下资源列表; 接口返回的`trackId`可以用于请求`/song/url/v1`接口,用于获取声音的下载地址 说明: 调用此接口, 获取标签下资源列表; 接口返回的`trackId`可以用于请求`/song/url/v1`接口,用于获取声音的下载地址
**必选参数 :** **必选参数 :**
`tag`: 标签, 由标签列表接口得到 `tag`: 标签, 由标签列表接口得到
@ -5250,7 +5238,7 @@ let data = encodeURIComponent(
说明: 调用此接口, 查看同类推荐 说明: 调用此接口, 查看同类推荐
**必选参数 :** **必选参数 :**
`id`: id, `/sati/tag/list`接口返回的`trackId` `id`: id, `/sati/tag/list`接口返回的`trackId`
@ -5270,7 +5258,7 @@ let data = encodeURIComponent(
说明: 调用此接口, 收藏声音 说明: 调用此接口, 收藏声音
**必选参数 :** **必选参数 :**
`id`: id, `/sati/tag/list`接口返回的`trackId` `id`: id, `/sati/tag/list`接口返回的`trackId`
@ -5286,7 +5274,7 @@ let data = encodeURIComponent(
说明: 调用此接口,获取跑步漫游的歌曲信息 说明: 调用此接口,获取跑步漫游的歌曲信息
**必选参数:** **必选参数:**
`bpm`: 步频 `bpm`: 步频

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')
} else if (statusRes.code === 802) {
updateStatus('二维码已扫描,请在手机上确认', 'waiting') updateStatus('二维码已扫描,请在手机上确认', 'waiting')
} else if (statusRes.code === 802) {
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')
} else if (statusRes.code === 802) {
updateStatus('二维码已扫描,请在手机上确认', 'waiting') updateStatus('二维码已扫描,请在手机上确认', 'waiting')
} else if (statusRes.code === 802) {
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

@ -1,575 +0,0 @@
#!/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)
* node scripts/format-docs.js --toc # 同时生成目录
*/
const fs = require('fs')
const path = require('path')
// ======================== 配置 ========================
const CONFIG = {
maxConsecutiveBlankLines: 2, // 最大连续空行数
codeBlockLang: true, // 代码块是否保留语言标记
headingSpaceBefore: true, // 标题前是否确保空行
headingSpaceAfter: true, // 标题后是否确保空行
listIndent: 2, // 列表缩进空格数
encodeSpecialChars: false, // 是否编码特殊字符
removeTrailingSpaces: true, // 是否删除行尾空格
tocMaxLevel: 3, // 目录最大标题层级
}
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 generateTOC(input, maxLevel = 3) {
const lines = input.split('\n')
const headings = []
for (const line of lines) {
const match = line.match(/^(#{1,6})\s+(.+)$/)
if (match) {
const level = match[1].length
if (level <= maxLevel) {
headings.push({ level, text: match[2].trim() })
}
}
}
if (headings.length === 0) return ''
let toc = '\n## 目录\n\n'
for (const h of headings) {
const indent = ' '.repeat(h.level - 1)
const anchor = h.text
.toLowerCase()
.replace(/[^\w\u4e00-\u9fa5]+/g, '-')
.replace(/^-|-$/g, '')
toc += `${indent}- [${h.text}](#${anchor})\n`
}
toc += '\n---\n'
return toc
}
// ======================== 统计信息 ========================
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) {
// 如果指定了 --toc在文档开头插入目录
let output = formatted
if (options.toc) {
const tocPlaceholder = '<!-- TOC -->'
if (output.includes(tocPlaceholder)) {
output = output.replace(
new RegExp(`${tocPlaceholder}[\\s\\S]*?${tocPlaceholder}`),
`${tocPlaceholder}\n${generateTOC(output, options.tocMaxLevel || CONFIG.tocMaxLevel)}${tocPlaceholder}`
)
} else {
output = generateTOC(output, options.tocMaxLevel || CONFIG.tocMaxLevel) + '\n' + output
}
}
writeFile(filePath, output)
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 模式,只检查不写入
--toc, -t 在文档中生成目录
--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
node scripts/format-docs.js --toc --verbose
`))
}
function parseArgs() {
const args = process.argv.slice(2)
const options = {
file: null,
dir: null,
check: false,
toc: 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 '--toc':
case '-t':
options.toc = 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, generateTOC, getStats, parseBlocks }

View File

@ -10,7 +10,6 @@ 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.
@ -300,15 +299,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
@ -318,6 +317,7 @@ 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,10 +327,7 @@ async function constructServer(moduleDefs) {
return request(...obj) return request(...obj)
}) })
const displayCrypto = usedCrypto || (APP_CONF.encrypt ? 'eapi' : 'api') logger.info(`Request Success: ${decode(req.originalUrl)}`)
logger.info(
`Request Success: [${displayCrypto}] ${decode(req.originalUrl)}`,
)
// 夹带私货部分如果开启了通用解锁并且是获取歌曲URL的接口则尝试解锁如果需要的话ヾ(≧▽≦*)o // 夹带私货部分如果开启了通用解锁并且是获取歌曲URL的接口则尝试解锁如果需要的话ヾ(≧▽≦*)o
if ( if (
@ -448,7 +445,10 @@ 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

@ -16,5 +16,9 @@
"Access-Control-Allow-Headers": "X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version" "Access-Control-Allow-Headers": "X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version"
} }
} }
] ],
"env": {
"NODE_ENV": "production",
"ENABLE_FLAC": "true"
}
} }