mirror of
https://github.com/NeteaseCloudMusicApiEnhanced/api-enhanced.git
synced 2026-06-27 21:25:08 +00:00
Compare commits
6 Commits
395a80e74b
...
6fc4231142
| Author | SHA1 | Date | |
|---|---|---|---|
| 6fc4231142 | |||
| fa69736300 | |||
| 73a5e9d788 | |||
| dc6f56e8a8 | |||
| 2c9da8507d | |||
| 1db354464f |
13
app.js
13
app.js
@ -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_token(Vercel 构建环境下跳过网络请求喵~)
|
// 启动时更新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)
|
|
||||||
})
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -11,8 +11,6 @@
|
|||||||
"prepare": "husky install",
|
"prepare": "husky install",
|
||||||
"docs:format": "node scripts/format-docs.js",
|
"docs:format": "node scripts/format-docs.js",
|
||||||
"docs:check": "node scripts/format-docs.js --check",
|
"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 +52,7 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://neteasecloudmusicapienhanced.js.org/",
|
"homepage": "https://neteasecloudmusicapienhanced.js.org/",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*.js": [
|
"*.js": [
|
||||||
|
|||||||
@ -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)的复刻版本
|
||||||
|
|||||||
@ -1,14 +1,14 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
/**
|
/**
|
||||||
* 📝 Markdown 文档格式化工具喵~
|
* 📝 Markdown 文档格式化工具喵~
|
||||||
* 支持格式化标题、代码块、空行、缩进,还能生成目录!
|
* 支持格式化标题、代码块、空行和缩进!
|
||||||
*
|
*
|
||||||
* 用法:
|
* 用法:
|
||||||
* node scripts/format-docs.js # 格式化默认文档 (public/docs/home.md)
|
* node scripts/format-docs.js # 格式化默认文档 (public/docs/home.md)
|
||||||
* node scripts/format-docs.js <文件路径> # 格式化指定文件
|
* node scripts/format-docs.js <文件路径> # 格式化指定文件
|
||||||
* node scripts/format-docs.js --dir <目录> # 格式化整个目录的 .md 文件
|
* node scripts/format-docs.js --dir <目录> # 格式化整个目录的 .md 文件
|
||||||
* node scripts/format-docs.js --check # 只检查不写入 (dry-run)
|
* node scripts/format-docs.js --check # 只检查不写入 (dry-run)
|
||||||
* node scripts/format-docs.js --toc # 同时生成目录
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
@ -16,14 +16,13 @@ const path = require('path')
|
|||||||
|
|
||||||
// ======================== 配置 ========================
|
// ======================== 配置 ========================
|
||||||
const CONFIG = {
|
const CONFIG = {
|
||||||
maxConsecutiveBlankLines: 2, // 最大连续空行数
|
maxConsecutiveBlankLines: 2, // 最大连续空行数
|
||||||
codeBlockLang: true, // 代码块是否保留语言标记
|
codeBlockLang: true, // 代码块是否保留语言标记
|
||||||
headingSpaceBefore: true, // 标题前是否确保空行
|
headingSpaceBefore: true, // 标题前是否确保空行
|
||||||
headingSpaceAfter: true, // 标题后是否确保空行
|
headingSpaceAfter: true, // 标题后是否确保空行
|
||||||
listIndent: 2, // 列表缩进空格数
|
listIndent: 2, // 列表缩进空格数
|
||||||
encodeSpecialChars: false, // 是否编码特殊字符
|
encodeSpecialChars: false, // 是否编码特殊字符
|
||||||
removeTrailingSpaces: true, // 是否删除行尾空格
|
removeTrailingSpaces: true, // 是否删除行尾空格
|
||||||
tocMaxLevel: 3, // 目录最大标题层级
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_FILE = path.resolve(__dirname, '..', 'public', 'docs', 'home.md')
|
const DEFAULT_FILE = path.resolve(__dirname, '..', 'public', 'docs', 'home.md')
|
||||||
@ -107,7 +106,10 @@ function parseBlocks(lines) {
|
|||||||
i++
|
i++
|
||||||
while (i < lines.length) {
|
while (i < lines.length) {
|
||||||
const t = lines[i].trim()
|
const t = lines[i].trim()
|
||||||
if (/-->/.test(t) || /<\/\w+>/.test(t)) { i++; break }
|
if (/-->/.test(t) || /<\/\w+>/.test(t)) {
|
||||||
|
i++
|
||||||
|
break
|
||||||
|
}
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
const htmlLines = lines.slice(start, i)
|
const htmlLines = lines.slice(start, i)
|
||||||
@ -123,17 +125,31 @@ function parseBlocks(lines) {
|
|||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
const quoteLines = lines.slice(start, i)
|
const quoteLines = lines.slice(start, i)
|
||||||
blocks.push({ type: 'blockquote', lines: quoteLines, raw: quoteLines.join('\n') })
|
blocks.push({
|
||||||
|
type: 'blockquote',
|
||||||
|
lines: quoteLines,
|
||||||
|
raw: quoteLines.join('\n'),
|
||||||
|
})
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// 表格
|
// 表格
|
||||||
if (/\|/.test(trimmed) && lines[i + 1] && /^\|[\s\-:]+\|/.test(lines[i + 1].trim())) {
|
if (
|
||||||
|
/\|/.test(trimmed) &&
|
||||||
|
lines[i + 1] &&
|
||||||
|
/^\|[\s\-:]+\|/.test(lines[i + 1].trim())
|
||||||
|
) {
|
||||||
const start = i
|
const start = i
|
||||||
i += 2
|
i += 2
|
||||||
while (i < lines.length && /\|/.test(lines[i].trim())) { i++ }
|
while (i < lines.length && /\|/.test(lines[i].trim())) {
|
||||||
|
i++
|
||||||
|
}
|
||||||
const tableLines = lines.slice(start, i)
|
const tableLines = lines.slice(start, i)
|
||||||
blocks.push({ type: 'table', lines: tableLines, raw: tableLines.join('\n') })
|
blocks.push({
|
||||||
|
type: 'table',
|
||||||
|
lines: tableLines,
|
||||||
|
raw: tableLines.join('\n'),
|
||||||
|
})
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -143,10 +159,19 @@ function parseBlocks(lines) {
|
|||||||
i++
|
i++
|
||||||
while (i < lines.length) {
|
while (i < lines.length) {
|
||||||
const t = lines[i].trim()
|
const t = lines[i].trim()
|
||||||
if (t === '') { i++; break }
|
if (t === '') {
|
||||||
if (/^(\s*[-*+]\s|\s*\d+\.\s)/.test(lines[i])) { i++; continue }
|
i++
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (/^(\s*[-*+]\s|\s*\d+\.\s)/.test(lines[i])) {
|
||||||
|
i++
|
||||||
|
continue
|
||||||
|
}
|
||||||
// 缩进 continuation
|
// 缩进 continuation
|
||||||
if (/^\s{2,}/.test(lines[i])) { i++; continue }
|
if (/^\s{2,}/.test(lines[i])) {
|
||||||
|
i++
|
||||||
|
continue
|
||||||
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
const listLines = lines.slice(start, i)
|
const listLines = lines.slice(start, i)
|
||||||
@ -160,13 +185,23 @@ function parseBlocks(lines) {
|
|||||||
while (i < lines.length && lines[i].trim() !== '') {
|
while (i < lines.length && lines[i].trim() !== '') {
|
||||||
// 如果遇到新的块元素则停止
|
// 如果遇到新的块元素则停止
|
||||||
const t = 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 (
|
||||||
|
/^#{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
|
if (/^(-{3,}|\*{3,}|_{3,})\s*$/.test(t)) break
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
const paraLines = lines.slice(start, i)
|
const paraLines = lines.slice(start, i)
|
||||||
blocks.push({ type: 'paragraph', lines: paraLines, raw: paraLines.join('\n') })
|
blocks.push({
|
||||||
|
type: 'paragraph',
|
||||||
|
lines: paraLines,
|
||||||
|
raw: paraLines.join('\n'),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return blocks
|
return blocks
|
||||||
@ -190,12 +225,20 @@ function formatMarkdown(input, options = {}) {
|
|||||||
const newLines = [...block.lines]
|
const newLines = [...block.lines]
|
||||||
|
|
||||||
// 标题前加空行(如果不是第一个块且前一个不是空行)
|
// 标题前加空行(如果不是第一个块且前一个不是空行)
|
||||||
if (cfg.headingSpaceBefore && idx > 0 && blocks[idx - 1].type !== 'empty') {
|
if (
|
||||||
|
cfg.headingSpaceBefore &&
|
||||||
|
idx > 0 &&
|
||||||
|
blocks[idx - 1].type !== 'empty'
|
||||||
|
) {
|
||||||
newLines.unshift('')
|
newLines.unshift('')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 标题后加空行(如果不是最后一个块且后一个不是空行)
|
// 标题后加空行(如果不是最后一个块且后一个不是空行)
|
||||||
if (cfg.headingSpaceAfter && idx < blocks.length - 1 && blocks[idx + 1].type !== 'empty') {
|
if (
|
||||||
|
cfg.headingSpaceAfter &&
|
||||||
|
idx < blocks.length - 1 &&
|
||||||
|
blocks[idx + 1].type !== 'empty'
|
||||||
|
) {
|
||||||
newLines.push('')
|
newLines.push('')
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -220,10 +263,18 @@ function formatMarkdown(input, options = {}) {
|
|||||||
newLines[0] = lang ? `${indent}${marker}${lang}` : `${indent}${marker}`
|
newLines[0] = lang ? `${indent}${marker}${lang}` : `${indent}${marker}`
|
||||||
|
|
||||||
// 确保代码块前后有空行(插在标准化之后,因为只动第一行)
|
// 确保代码块前后有空行(插在标准化之后,因为只动第一行)
|
||||||
if (idx > 0 && blocks[idx - 1].type !== 'empty' && blocks[idx - 1].type !== 'code') {
|
if (
|
||||||
|
idx > 0 &&
|
||||||
|
blocks[idx - 1].type !== 'empty' &&
|
||||||
|
blocks[idx - 1].type !== 'code'
|
||||||
|
) {
|
||||||
newLines.unshift('')
|
newLines.unshift('')
|
||||||
}
|
}
|
||||||
if (idx < blocks.length - 1 && blocks[idx + 1].type !== 'empty' && blocks[idx + 1].type !== 'code') {
|
if (
|
||||||
|
idx < blocks.length - 1 &&
|
||||||
|
blocks[idx + 1].type !== 'empty' &&
|
||||||
|
blocks[idx + 1].type !== 'code'
|
||||||
|
) {
|
||||||
newLines.push('')
|
newLines.push('')
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -253,7 +304,8 @@ function formatMarkdown(input, options = {}) {
|
|||||||
const indent = line.length - line.trimStart().length
|
const indent = line.length - line.trimStart().length
|
||||||
// 如果是顶层列表项,确保缩进为0
|
// 如果是顶层列表项,确保缩进为0
|
||||||
if (indent % cfg.listIndent !== 0) {
|
if (indent % cfg.listIndent !== 0) {
|
||||||
const normalizedIndent = Math.round(indent / cfg.listIndent) * cfg.listIndent
|
const normalizedIndent =
|
||||||
|
Math.round(indent / cfg.listIndent) * cfg.listIndent
|
||||||
return ' '.repeat(normalizedIndent) + trimmed
|
return ' '.repeat(normalizedIndent) + trimmed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -267,7 +319,8 @@ function formatMarkdown(input, options = {}) {
|
|||||||
|
|
||||||
// 处理文件开头和结尾的空行
|
// 处理文件开头和结尾的空行
|
||||||
while (resultLines.length > 0 && resultLines[0] === '') resultLines.shift()
|
while (resultLines.length > 0 && resultLines[0] === '') resultLines.shift()
|
||||||
while (resultLines.length > 0 && resultLines[resultLines.length - 1] === '') resultLines.pop()
|
while (resultLines.length > 0 && resultLines[resultLines.length - 1] === '')
|
||||||
|
resultLines.pop()
|
||||||
resultLines.push('') // 文件结尾留一个空行
|
resultLines.push('') // 文件结尾留一个空行
|
||||||
|
|
||||||
return resultLines.join('\n')
|
return resultLines.join('\n')
|
||||||
@ -295,41 +348,6 @@ function compressEmptyLines(blocks, maxBlank) {
|
|||||||
return result
|
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) {
|
function getStats(input) {
|
||||||
@ -417,20 +435,7 @@ function processFile(filePath, options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (hasChanges) {
|
if (hasChanges) {
|
||||||
// 如果指定了 --toc,在文档开头插入目录
|
writeFile(filePath, formatted)
|
||||||
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(' ✨ 格式化完成!'))
|
console.log(green(' ✨ 格式化完成!'))
|
||||||
if (options.verbose) {
|
if (options.verbose) {
|
||||||
printStats(statsAfter)
|
printStats(statsAfter)
|
||||||
@ -454,7 +459,9 @@ function processDirectory(dirPath, options) {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(bold(`\n📁 扫描目录: ${cyan(dirPath)} (${files.length} 个文件)\n`))
|
console.log(
|
||||||
|
bold(`\n📁 扫描目录: ${cyan(dirPath)} (${files.length} 个文件)\n`),
|
||||||
|
)
|
||||||
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
const ret = processFile(file, options)
|
const ret = processFile(file, options)
|
||||||
@ -467,7 +474,8 @@ function processDirectory(dirPath, options) {
|
|||||||
// ======================== CLI ========================
|
// ======================== CLI ========================
|
||||||
|
|
||||||
function printHelp() {
|
function printHelp() {
|
||||||
console.log(bold(`
|
console.log(
|
||||||
|
bold(`
|
||||||
📝 Markdown 文档格式化工具 v1.0.0
|
📝 Markdown 文档格式化工具 v1.0.0
|
||||||
|
|
||||||
${cyan('用法:')}
|
${cyan('用法:')}
|
||||||
@ -479,7 +487,6 @@ ${cyan('参数:')}
|
|||||||
${cyan('选项:')}
|
${cyan('选项:')}
|
||||||
--dir, -d <目录> 格式化整个目录下的所有 .md 文件
|
--dir, -d <目录> 格式化整个目录下的所有 .md 文件
|
||||||
--check, -c dry-run 模式,只检查不写入
|
--check, -c dry-run 模式,只检查不写入
|
||||||
--toc, -t 在文档中生成目录
|
|
||||||
--verbose, -v 显示详细统计信息
|
--verbose, -v 显示详细统计信息
|
||||||
--help, -h 显示帮助信息
|
--help, -h 显示帮助信息
|
||||||
|
|
||||||
@ -488,8 +495,8 @@ ${cyan('示例:')}
|
|||||||
node scripts/format-docs.js README.md
|
node scripts/format-docs.js README.md
|
||||||
node scripts/format-docs.js --dir docs/
|
node scripts/format-docs.js --dir docs/
|
||||||
node scripts/format-docs.js --check
|
node scripts/format-docs.js --check
|
||||||
node scripts/format-docs.js --toc --verbose
|
`),
|
||||||
`))
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseArgs() {
|
function parseArgs() {
|
||||||
@ -498,7 +505,6 @@ function parseArgs() {
|
|||||||
file: null,
|
file: null,
|
||||||
dir: null,
|
dir: null,
|
||||||
check: false,
|
check: false,
|
||||||
toc: false,
|
|
||||||
verbose: false,
|
verbose: false,
|
||||||
help: false,
|
help: false,
|
||||||
}
|
}
|
||||||
@ -513,10 +519,6 @@ function parseArgs() {
|
|||||||
case '-c':
|
case '-c':
|
||||||
options.check = true
|
options.check = true
|
||||||
break
|
break
|
||||||
case '--toc':
|
|
||||||
case '-t':
|
|
||||||
options.toc = true
|
|
||||||
break
|
|
||||||
case '--verbose':
|
case '--verbose':
|
||||||
case '-v':
|
case '-v':
|
||||||
options.verbose = true
|
options.verbose = true
|
||||||
@ -572,4 +574,4 @@ if (require.main === module) {
|
|||||||
process.exit(exitCode)
|
process.exit(exitCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { formatMarkdown, generateTOC, getStats, parseBlocks }
|
module.exports = { formatMarkdown, getStats, parseBlocks }
|
||||||
|
|||||||
16
server.js
16
server.js
@ -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
|
||||||
|
|||||||
@ -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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user