mirror of
https://github.com/NeteaseCloudMusicApiEnhanced/api-enhanced.git
synced 2026-06-27 21:25:08 +00:00
Compare commits
No commits in common. "6fc4231142e2a91913cdf54f27cc9722391e30c0" and "395a80e74b9e69b8bfadfc19d77d57121f68087d" have entirely different histories.
6fc4231142
...
395a80e74b
13
app.js
13
app.js
@ -8,11 +8,16 @@ 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
|
// 启动时更新anonymous_token(Vercel 构建环境下跳过网络请求喵~)
|
||||||
const generateConfig = require('./generateConfig')
|
if (!process.env.VERCEL_ENV) {
|
||||||
await generateConfig()
|
const generateConfig = require('./generateConfig')
|
||||||
|
await generateConfig()
|
||||||
|
}
|
||||||
require('./server').serveNcmApi({
|
require('./server').serveNcmApi({
|
||||||
checkVersion: true,
|
checkVersion: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
start()
|
start().catch((err) => {
|
||||||
|
console.error('[FATAL] 启动失败:', err)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
|
|||||||
@ -4,38 +4,159 @@ 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')
|
||||||
|
|
||||||
async function generateConfig() {
|
const MAX_RETRIES = 10
|
||||||
global.cnIp = generateRandomChineseIP()
|
const RETRY_DELAY_MS = 1000
|
||||||
try {
|
|
||||||
const res = await register_anonimous()
|
function sleep(ms) {
|
||||||
const cookie = res.body.cookie
|
return new Promise((resolve) => setTimeout(resolve, ms))
|
||||||
if (cookie) {
|
}
|
||||||
const cookieObj = cookieToJson(cookie)
|
|
||||||
|
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(
|
fs.writeFileSync(
|
||||||
path.resolve(tmpPath, 'anonymous_token'),
|
path.resolve(tmpPath, 'xeapi_public_key'),
|
||||||
cookieObj.MUSIC_A,
|
JSON.stringify(publicKey),
|
||||||
'utf-8',
|
'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 }
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.log(error)
|
|
||||||
}
|
}
|
||||||
try {
|
return { success: false, error: new Error('unreachable') }
|
||||||
let currentPublicKey = {}
|
}
|
||||||
try {
|
|
||||||
currentPublicKey = JSON.parse(
|
/**
|
||||||
fs.readFileSync(path.resolve(tmpPath, 'xeapi_public_key'), 'utf-8'),
|
* 生成配置(匿名 token + xeapi public key),带容错重试
|
||||||
)
|
* @returns {{ tokenOk: boolean, keyOk: boolean }}
|
||||||
} catch (_) {}
|
*/
|
||||||
const publicKey = await getXeapiPublicKey(currentPublicKey, global.deviceId)
|
async function generateConfig() {
|
||||||
fs.writeFileSync(
|
global.cnIp = generateRandomChineseIP()
|
||||||
path.resolve(tmpPath, 'xeapi_public_key'),
|
|
||||||
JSON.stringify(publicKey),
|
// 两个任务并行执行,互不影响喵~
|
||||||
'utf-8',
|
const [tokenResult, keyResult] = await Promise.all([
|
||||||
)
|
fetchAnonymousToken(),
|
||||||
} catch (error) {
|
fetchXeapiPublicKey(),
|
||||||
console.log(error)
|
])
|
||||||
|
|
||||||
|
if (!tokenResult.success) {
|
||||||
|
logger.warn('[generateConfig] 匿名 token 获取失败')
|
||||||
|
}
|
||||||
|
if (!keyResult.success) {
|
||||||
|
logger.warn('[generateConfig] xeapi public key 获取失败')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tokenResult.success && keyResult.success) {
|
||||||
|
logger.success('[generateConfig] 配置初始化完成')
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
tokenOk: tokenResult.success,
|
||||||
|
keyOk: keyResult.success,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
module.exports = generateConfig
|
module.exports = generateConfig
|
||||||
|
|||||||
@ -26,7 +26,6 @@ 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,6 +11,8 @@
|
|||||||
"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"
|
||||||
@ -52,7 +54,7 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://neteasecloudmusicapienhanced.js.org/",
|
"homepage": "https://neteasecloudmusicapienhanced.js.org/",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=22"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*.js": [
|
"*.js": [
|
||||||
|
|||||||
@ -1,3 +1,9 @@
|
|||||||
|
## 目录
|
||||||
|
|
||||||
|
- [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,13 +16,14 @@ 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')
|
||||||
@ -106,10 +107,7 @@ 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)) {
|
if (/-->/.test(t) || /<\/\w+>/.test(t)) { i++; break }
|
||||||
i++
|
|
||||||
break
|
|
||||||
}
|
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
const htmlLines = lines.slice(start, i)
|
const htmlLines = lines.slice(start, i)
|
||||||
@ -125,31 +123,17 @@ function parseBlocks(lines) {
|
|||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
const quoteLines = lines.slice(start, i)
|
const quoteLines = lines.slice(start, i)
|
||||||
blocks.push({
|
blocks.push({ type: 'blockquote', lines: quoteLines, raw: quoteLines.join('\n') })
|
||||||
type: 'blockquote',
|
|
||||||
lines: quoteLines,
|
|
||||||
raw: quoteLines.join('\n'),
|
|
||||||
})
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// 表格
|
// 表格
|
||||||
if (
|
if (/\|/.test(trimmed) && lines[i + 1] && /^\|[\s\-:]+\|/.test(lines[i + 1].trim())) {
|
||||||
/\|/.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())) {
|
while (i < lines.length && /\|/.test(lines[i].trim())) { i++ }
|
||||||
i++
|
|
||||||
}
|
|
||||||
const tableLines = lines.slice(start, i)
|
const tableLines = lines.slice(start, i)
|
||||||
blocks.push({
|
blocks.push({ type: 'table', lines: tableLines, raw: tableLines.join('\n') })
|
||||||
type: 'table',
|
|
||||||
lines: tableLines,
|
|
||||||
raw: tableLines.join('\n'),
|
|
||||||
})
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -159,19 +143,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 (t === '') {
|
if (t === '') { i++; break }
|
||||||
i++
|
if (/^(\s*[-*+]\s|\s*\d+\.\s)/.test(lines[i])) { i++; continue }
|
||||||
break
|
|
||||||
}
|
|
||||||
if (/^(\s*[-*+]\s|\s*\d+\.\s)/.test(lines[i])) {
|
|
||||||
i++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// 缩进 continuation
|
// 缩进 continuation
|
||||||
if (/^\s{2,}/.test(lines[i])) {
|
if (/^\s{2,}/.test(lines[i])) { i++; continue }
|
||||||
i++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
const listLines = lines.slice(start, i)
|
const listLines = lines.slice(start, i)
|
||||||
@ -185,23 +160,13 @@ 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 (
|
if (/^#{1,6}\s/.test(t) || /^```/.test(t) || /^~~~/.test(t) || /^(\s*[-*+]\s|\s*\d+\.\s)/.test(lines[i])) break
|
||||||
/^#{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({
|
blocks.push({ type: 'paragraph', lines: paraLines, raw: paraLines.join('\n') })
|
||||||
type: 'paragraph',
|
|
||||||
lines: paraLines,
|
|
||||||
raw: paraLines.join('\n'),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return blocks
|
return blocks
|
||||||
@ -225,20 +190,12 @@ function formatMarkdown(input, options = {}) {
|
|||||||
const newLines = [...block.lines]
|
const newLines = [...block.lines]
|
||||||
|
|
||||||
// 标题前加空行(如果不是第一个块且前一个不是空行)
|
// 标题前加空行(如果不是第一个块且前一个不是空行)
|
||||||
if (
|
if (cfg.headingSpaceBefore && idx > 0 && blocks[idx - 1].type !== 'empty') {
|
||||||
cfg.headingSpaceBefore &&
|
|
||||||
idx > 0 &&
|
|
||||||
blocks[idx - 1].type !== 'empty'
|
|
||||||
) {
|
|
||||||
newLines.unshift('')
|
newLines.unshift('')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 标题后加空行(如果不是最后一个块且后一个不是空行)
|
// 标题后加空行(如果不是最后一个块且后一个不是空行)
|
||||||
if (
|
if (cfg.headingSpaceAfter && idx < blocks.length - 1 && blocks[idx + 1].type !== 'empty') {
|
||||||
cfg.headingSpaceAfter &&
|
|
||||||
idx < blocks.length - 1 &&
|
|
||||||
blocks[idx + 1].type !== 'empty'
|
|
||||||
) {
|
|
||||||
newLines.push('')
|
newLines.push('')
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -263,18 +220,10 @@ function formatMarkdown(input, options = {}) {
|
|||||||
newLines[0] = lang ? `${indent}${marker}${lang}` : `${indent}${marker}`
|
newLines[0] = lang ? `${indent}${marker}${lang}` : `${indent}${marker}`
|
||||||
|
|
||||||
// 确保代码块前后有空行(插在标准化之后,因为只动第一行)
|
// 确保代码块前后有空行(插在标准化之后,因为只动第一行)
|
||||||
if (
|
if (idx > 0 && blocks[idx - 1].type !== 'empty' && blocks[idx - 1].type !== 'code') {
|
||||||
idx > 0 &&
|
|
||||||
blocks[idx - 1].type !== 'empty' &&
|
|
||||||
blocks[idx - 1].type !== 'code'
|
|
||||||
) {
|
|
||||||
newLines.unshift('')
|
newLines.unshift('')
|
||||||
}
|
}
|
||||||
if (
|
if (idx < blocks.length - 1 && blocks[idx + 1].type !== 'empty' && blocks[idx + 1].type !== 'code') {
|
||||||
idx < blocks.length - 1 &&
|
|
||||||
blocks[idx + 1].type !== 'empty' &&
|
|
||||||
blocks[idx + 1].type !== 'code'
|
|
||||||
) {
|
|
||||||
newLines.push('')
|
newLines.push('')
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -304,8 +253,7 @@ 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 =
|
const normalizedIndent = Math.round(indent / cfg.listIndent) * cfg.listIndent
|
||||||
Math.round(indent / cfg.listIndent) * cfg.listIndent
|
|
||||||
return ' '.repeat(normalizedIndent) + trimmed
|
return ' '.repeat(normalizedIndent) + trimmed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -319,8 +267,7 @@ 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] === '')
|
while (resultLines.length > 0 && resultLines[resultLines.length - 1] === '') resultLines.pop()
|
||||||
resultLines.pop()
|
|
||||||
resultLines.push('') // 文件结尾留一个空行
|
resultLines.push('') // 文件结尾留一个空行
|
||||||
|
|
||||||
return resultLines.join('\n')
|
return resultLines.join('\n')
|
||||||
@ -348,6 +295,41 @@ 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) {
|
||||||
@ -435,7 +417,20 @@ function processFile(filePath, options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (hasChanges) {
|
if (hasChanges) {
|
||||||
writeFile(filePath, formatted)
|
// 如果指定了 --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(' ✨ 格式化完成!'))
|
console.log(green(' ✨ 格式化完成!'))
|
||||||
if (options.verbose) {
|
if (options.verbose) {
|
||||||
printStats(statsAfter)
|
printStats(statsAfter)
|
||||||
@ -459,9 +454,7 @@ function processDirectory(dirPath, options) {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(
|
console.log(bold(`\n📁 扫描目录: ${cyan(dirPath)} (${files.length} 个文件)\n`))
|
||||||
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)
|
||||||
@ -474,8 +467,7 @@ function processDirectory(dirPath, options) {
|
|||||||
// ======================== CLI ========================
|
// ======================== CLI ========================
|
||||||
|
|
||||||
function printHelp() {
|
function printHelp() {
|
||||||
console.log(
|
console.log(bold(`
|
||||||
bold(`
|
|
||||||
📝 Markdown 文档格式化工具 v1.0.0
|
📝 Markdown 文档格式化工具 v1.0.0
|
||||||
|
|
||||||
${cyan('用法:')}
|
${cyan('用法:')}
|
||||||
@ -487,6 +479,7 @@ ${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 显示帮助信息
|
||||||
|
|
||||||
@ -495,8 +488,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() {
|
||||||
@ -505,6 +498,7 @@ function parseArgs() {
|
|||||||
file: null,
|
file: null,
|
||||||
dir: null,
|
dir: null,
|
||||||
check: false,
|
check: false,
|
||||||
|
toc: false,
|
||||||
verbose: false,
|
verbose: false,
|
||||||
help: false,
|
help: false,
|
||||||
}
|
}
|
||||||
@ -519,6 +513,10 @@ 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
|
||||||
@ -574,4 +572,4 @@ if (require.main === module) {
|
|||||||
process.exit(exitCode)
|
process.exit(exitCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { formatMarkdown, getStats, parseBlocks }
|
module.exports = { formatMarkdown, generateTOC, getStats, parseBlocks }
|
||||||
|
|||||||
16
server.js
16
server.js
@ -10,6 +10,7 @@ const { cookieToJson } = require('./util/index')
|
|||||||
const fileUpload = require('express-fileupload')
|
const fileUpload = require('express-fileupload')
|
||||||
const decode = require('safe-decode-uri-component')
|
const decode = require('safe-decode-uri-component')
|
||||||
const logger = require('./util/logger.js')
|
const logger = require('./util/logger.js')
|
||||||
|
const { APP_CONF } = require('./util/config.json')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The version check result.
|
* The version check result.
|
||||||
@ -299,15 +300,15 @@ async function constructServer(moduleDefs) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
let usedCrypto = ''
|
||||||
const moduleResponse = await moduleDef.module(query, (...params) => {
|
const moduleResponse = await moduleDef.module(query, (...params) => {
|
||||||
// 参数注入客户端IP
|
|
||||||
const obj = [...params]
|
const obj = [...params]
|
||||||
const options = obj[2] || {}
|
const options = obj[2] || {}
|
||||||
|
usedCrypto = options.crypto || ''
|
||||||
let ip = ''
|
let ip = ''
|
||||||
|
|
||||||
if (options.randomCNIP) {
|
if (options.randomCNIP) {
|
||||||
ip = global.cnIp
|
ip = global.cnIp
|
||||||
// logger.info('Using random Chinese IP for request:', ip)
|
|
||||||
} else {
|
} else {
|
||||||
ip = req.ip
|
ip = req.ip
|
||||||
|
|
||||||
@ -317,7 +318,6 @@ async function constructServer(moduleDefs) {
|
|||||||
if (ip == '::1') {
|
if (ip == '::1') {
|
||||||
ip = global.cnIp
|
ip = global.cnIp
|
||||||
}
|
}
|
||||||
// logger.info('Requested from ip:', ip)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
obj[2] = {
|
obj[2] = {
|
||||||
@ -327,7 +327,10 @@ async function constructServer(moduleDefs) {
|
|||||||
|
|
||||||
return request(...obj)
|
return request(...obj)
|
||||||
})
|
})
|
||||||
logger.info(`Request Success: ${decode(req.originalUrl)}`)
|
const displayCrypto = usedCrypto || (APP_CONF.encrypt ? 'eapi' : 'api')
|
||||||
|
logger.info(
|
||||||
|
`Request Success: [${displayCrypto}] ${decode(req.originalUrl)}`,
|
||||||
|
)
|
||||||
|
|
||||||
// 夹带私货部分:如果开启了通用解锁,并且是获取歌曲URL的接口,则尝试解锁(如果需要的话)ヾ(≧▽≦*)o
|
// 夹带私货部分:如果开启了通用解锁,并且是获取歌曲URL的接口,则尝试解锁(如果需要的话)ヾ(≧▽≦*)o
|
||||||
if (
|
if (
|
||||||
@ -445,10 +448,7 @@ 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,9 +16,5 @@
|
|||||||
"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