mirror of
https://github.com/NeteaseCloudMusicApiEnhanced/api-enhanced.git
synced 2026-06-28 05:35:08 +00:00
Compare commits
No commits in common. "main" and "v4.33.1" have entirely different histories.
43
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
43
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@ -14,17 +14,17 @@ body:
|
|||||||
id: terms
|
id: terms
|
||||||
attributes:
|
attributes:
|
||||||
label: 确认事项
|
label: 确认事项
|
||||||
description: |
|
description: 在提交Bug报告前,请确认以下事项
|
||||||
在提交Bug报告前,请确认以下事项:
|
|
||||||
|
|
||||||
- 我已经搜索了现有的issues,确认这不是重复问题
|
|
||||||
- 我使用的是最新版本的API, 而且是官方发布的版本,而不是fork或修改版
|
|
||||||
- 不处理别人搭建的线上服务的问题,此项目提供任何线上服务不保证质量
|
|
||||||
- 如果不是提建议,提 issues 如果不照着模版来将不会优先处理或放着不管
|
|
||||||
- 维护项目都是业余时间,精力有限,我只能挑容易解决的issues处理,为了节约双方时间,请尽可能提供足够的有用的信息,给的信息不够我只能根据精力和时间看情况处理,如果模板信息看都不看就删掉,我不会进行任何回复,并且一个月后close掉issue
|
|
||||||
|
|
||||||
options:
|
options:
|
||||||
- label: 我已确认以上事项
|
- label: 我已经搜索了现有的issues,确认这不是重复问题
|
||||||
|
required: true
|
||||||
|
- label: 我使用的是最新版本的API, 而且是官方发布的版本,而不是fork或修改版
|
||||||
|
required: true
|
||||||
|
- label: 不处理别人搭建的线上服务的问题,此项目提供任何线上服务不保证质量
|
||||||
|
required: true
|
||||||
|
- label: 如果不是提建议,提 issues 如果不照着模版来将不会优先处理或放着不管
|
||||||
|
required: true
|
||||||
|
- label: 维护项目都是业余时间,精力有限,我只能挑容易解决的issues处理,为了节约双方时间,请尽可能提供足够的有用的信息,给的信息不够我只能根据精力和时间看情况处理,如果模板信息看都不看就删掉,我不会进行任何回复,并且一个月后close掉issue
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
- type: input
|
- type: input
|
||||||
@ -36,14 +36,29 @@ body:
|
|||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: os
|
||||||
|
attributes:
|
||||||
|
label: 操作系统或平台
|
||||||
|
description: 您在哪个操作系统上遇到了这个问题?
|
||||||
|
options:
|
||||||
|
- Windows 10
|
||||||
|
- Windows 11
|
||||||
|
- Ubuntu 20.04
|
||||||
|
- Ubuntu 22.04
|
||||||
|
- macOS
|
||||||
|
- 其他 Linux 发行版
|
||||||
|
- 使用部署平台或其他 (请在描述中说明)
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
id: deployment
|
id: deployment
|
||||||
attributes:
|
attributes:
|
||||||
label: 部署方式
|
label: 部署平台
|
||||||
description: 此项目支持本地部署和云平台部署,您使用的是哪种方式?
|
description:
|
||||||
options:
|
options:
|
||||||
- 本机直接运行 (node / pm2)
|
- 我使用的自己的服务器部署
|
||||||
- 本机 Docker 部署
|
|
||||||
- Vercel
|
- Vercel
|
||||||
- Heroku
|
- Heroku
|
||||||
- Railway
|
- Railway
|
||||||
|
|||||||
3
.github/ISSUE_TEMPLATE/config.yml
vendored
3
.github/ISSUE_TEMPLATE/config.yml
vendored
@ -1,8 +1,5 @@
|
|||||||
blank_issues_enabled: true
|
blank_issues_enabled: true
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: ↑请尽量使用议题模板创建议题↑
|
|
||||||
url: https://github.com/NeteaseCloudMusicApiEnhanced/api-enhanced/issues/new/choose
|
|
||||||
about: 选择适合的议题模板可以帮助我们更快地定位和解决问题
|
|
||||||
- name: 提问的艺术
|
- name: 提问的艺术
|
||||||
url: https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/main/README-zh_CN.md
|
url: https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/main/README-zh_CN.md
|
||||||
about: 默认所有 Issues 发起者均已了解此处的内容
|
about: 默认所有 Issues 发起者均已了解此处的内容
|
||||||
17
.github/workflows/release-on-version-change.yml
vendored
17
.github/workflows/release-on-version-change.yml
vendored
@ -136,7 +136,7 @@ jobs:
|
|||||||
esac
|
esac
|
||||||
|
|
||||||
- name: Upload build artifact
|
- name: Upload build artifact
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: ${{ matrix.platform }}-binary
|
name: ${{ matrix.platform }}-binary
|
||||||
path: release-artifacts/*
|
path: release-artifacts/*
|
||||||
@ -165,16 +165,17 @@ jobs:
|
|||||||
fetch-tags: true
|
fetch-tags: true
|
||||||
|
|
||||||
- name: Download build artifacts
|
- name: Download build artifacts
|
||||||
uses: actions/download-artifact@v8
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
path: release-artifacts
|
path: release-artifacts
|
||||||
merge-multiple: true
|
|
||||||
|
|
||||||
- name: List flattened artifacts
|
- name: Flatten artifacts
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
mkdir -p final-artifacts
|
mkdir -p final-artifacts
|
||||||
cp release-artifacts/* final-artifacts/
|
|
||||||
|
find release-artifacts -type f -exec cp {} final-artifacts/ \;
|
||||||
|
|
||||||
ls -lah final-artifacts
|
ls -lah final-artifacts
|
||||||
|
|
||||||
- name: Generate release notes
|
- name: Generate release notes
|
||||||
@ -260,13 +261,13 @@ jobs:
|
|||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Log in to Docker Hub
|
- name: Log in to Docker Hub
|
||||||
uses: docker/login-action@v4
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Log in to GitHub Container Registry
|
- name: Log in to GitHub Container Registry
|
||||||
uses: docker/login-action@v4
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
@ -312,4 +313,4 @@ jobs:
|
|||||||
|
|
||||||
- run: pnpm install --frozen-lockfile
|
- run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
- run: pnpm publish --access public --provenance
|
- run: pnpm publish --access public
|
||||||
2
.github/workflows/sync.yml
vendored
2
.github/workflows/sync.yml
vendored
@ -22,7 +22,7 @@ jobs:
|
|||||||
# Step 2: run the sync action
|
# Step 2: run the sync action
|
||||||
- name: Sync upstream changes
|
- name: Sync upstream changes
|
||||||
id: sync
|
id: sync
|
||||||
uses: aormsby/Fork-Sync-With-Upstream-action@v3.4.3
|
uses: aormsby/Fork-Sync-With-Upstream-action@v3.4.2
|
||||||
with:
|
with:
|
||||||
upstream_sync_repo: NeteaseCloudMusicApiEnhanced/api-enhanced
|
upstream_sync_repo: NeteaseCloudMusicApiEnhanced/api-enhanced
|
||||||
upstream_sync_branch: main
|
upstream_sync_branch: main
|
||||||
|
|||||||
23
AGENTS.md
23
AGENTS.md
@ -1,23 +0,0 @@
|
|||||||
# 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.
|
|
||||||
1418
CHANGELOG.MD
Normal file
1418
CHANGELOG.MD
Normal file
File diff suppressed because it is too large
Load Diff
@ -30,7 +30,7 @@
|
|||||||
|
|
||||||
## 项目简介
|
## 项目简介
|
||||||
|
|
||||||
网易云音乐第三方 Node.js API, 支持丰富的音乐相关接口,适合自建服务、二次开发和多平台部署
|
网易云音乐第三方 Node.js API, 支持丰富的音乐相关接口,适合自建服务、二次开发和多平台部署(如果原版诈尸, 我会及时同步 or 归档)。
|
||||||
|
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
>
|
>
|
||||||
@ -215,7 +215,7 @@ pnpm test
|
|||||||
|
|
||||||
原作者 [Binaryify/NeteaseCloudMusicApi](https://github.com/binaryify/NeteaseCloudMusicApi) 项目为本项目基础 (该项目在`npmjs`网站上仍持续维护, 但 github 仓库已不再更新)
|
原作者 [Binaryify/NeteaseCloudMusicApi](https://github.com/binaryify/NeteaseCloudMusicApi) 项目为本项目基础 (该项目在`npmjs`网站上仍持续维护, 但 github 仓库已不再更新)
|
||||||
|
|
||||||
感谢大佬们为逆向eapi, weapi, xeapi等加密算法所做的贡献
|
感谢大佬们为逆向eapi, weapi等加密算法所做的贡献
|
||||||
|
|
||||||
项目参考:
|
项目参考:
|
||||||
|
|
||||||
@ -225,8 +225,6 @@ pnpm test
|
|||||||
|
|
||||||
[chaunsin/netease-cloud-music](https://github.com/chaunsin/netease-cloud-music)
|
[chaunsin/netease-cloud-music](https://github.com/chaunsin/netease-cloud-music)
|
||||||
|
|
||||||
[folltoshe/netease-report-listen-song](https://github.com/folltoshe/netease-report-listen-song)
|
|
||||||
|
|
||||||
### SDK 生态
|
### SDK 生态
|
||||||
|
|
||||||
| 语言 | 作者 | 地址 | 类型 |
|
| 语言 | 作者 | 地址 | 类型 |
|
||||||
@ -246,8 +244,6 @@ pnpm test
|
|||||||
|
|
||||||
- [Yueby/music-together](https://github.com/Yueby/music-together)
|
- [Yueby/music-together](https://github.com/Yueby/music-together)
|
||||||
|
|
||||||
- [chthollyphlie/folia-major](https://github.com/chthollyphile/folia-major)
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
[MIT License](https://github.com/MoeFurina/NeteaseCloudMusicApiEnhanced/blob/main/LICENSE)
|
[MIT License](https://github.com/MoeFurina/NeteaseCloudMusicApiEnhanced/blob/main/LICENSE)
|
||||||
|
|||||||
@ -2,7 +2,6 @@ const fs = require('fs')
|
|||||||
const path = require('path')
|
const path = require('path')
|
||||||
const { register_anonimous } = require('./main')
|
const { register_anonimous } = require('./main')
|
||||||
const { cookieToJson, generateRandomChineseIP } = require('./util/index')
|
const { cookieToJson, generateRandomChineseIP } = require('./util/index')
|
||||||
const { getXeapiPublicKey } = require('./util/xeapiKey')
|
|
||||||
const tmpPath = require('os').tmpdir()
|
const tmpPath = require('os').tmpdir()
|
||||||
|
|
||||||
async function generateConfig() {
|
async function generateConfig() {
|
||||||
@ -21,21 +20,5 @@ async function generateConfig() {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
console.log(error)
|
||||||
}
|
}
|
||||||
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',
|
|
||||||
)
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
module.exports = generateConfig
|
module.exports = generateConfig
|
||||||
|
|||||||
721
interface.d.ts
vendored
721
interface.d.ts
vendored
@ -1841,724 +1841,3 @@ export function voice_lyric(
|
|||||||
id: number | string
|
id: number | string
|
||||||
} & RequestBaseConfig,
|
} & RequestBaseConfig,
|
||||||
): Promise<Response>
|
): Promise<Response>
|
||||||
|
|
||||||
export function aidj_content_rcmd(
|
|
||||||
params: {
|
|
||||||
latitude?: string | number
|
|
||||||
longitude?: string | number
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function album_privilege(
|
|
||||||
params: {
|
|
||||||
id: string | number
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function artist_detail_dynamic(
|
|
||||||
params: {
|
|
||||||
id: string | number
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function artist_follow_count(
|
|
||||||
params: {
|
|
||||||
id: string | number
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function broadcast_category_region_get(
|
|
||||||
params: RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function broadcast_channel_collect_list(
|
|
||||||
params: MultiPageConfig & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function broadcast_channel_currentinfo(
|
|
||||||
params: {
|
|
||||||
id: string | number
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function broadcast_channel_list(
|
|
||||||
params: {
|
|
||||||
categoryId?: string | number
|
|
||||||
regionId?: string | number
|
|
||||||
lastId?: string | number
|
|
||||||
score?: string | number
|
|
||||||
} & MultiPageConfig &
|
|
||||||
RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function broadcast_sub(
|
|
||||||
params: {
|
|
||||||
t: SubAction
|
|
||||||
id: string | number
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function chart_detail(
|
|
||||||
params: {
|
|
||||||
chartCode: string | number
|
|
||||||
targetId: string | number
|
|
||||||
targetType: string | number
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function chart_song_detail(
|
|
||||||
params: {
|
|
||||||
chartCode: string | number
|
|
||||||
targetId: string | number
|
|
||||||
targetType: string | number
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function cloud_import(
|
|
||||||
params: {
|
|
||||||
md5: string
|
|
||||||
id?: string | number
|
|
||||||
bitrate?: string | number
|
|
||||||
fileSize?: string | number
|
|
||||||
artist?: string
|
|
||||||
album?: string
|
|
||||||
song?: string
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function cloud_lyric_get(
|
|
||||||
params: {
|
|
||||||
uid: string | number
|
|
||||||
sid: string | number
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function cloud_upload_complete(
|
|
||||||
params: {
|
|
||||||
songId: string | number
|
|
||||||
resourceId: string | number
|
|
||||||
md5: string
|
|
||||||
filename: string
|
|
||||||
song?: string
|
|
||||||
artist?: string
|
|
||||||
album?: string
|
|
||||||
bitrate?: string | number
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function cloud_upload_token(
|
|
||||||
params: {
|
|
||||||
md5: string
|
|
||||||
fileSize: string | number
|
|
||||||
filename: string
|
|
||||||
bitrate?: string | number
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function comment_info_list(
|
|
||||||
params: {
|
|
||||||
ids?: string
|
|
||||||
type?: CommentType
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function comment_report(
|
|
||||||
params: {
|
|
||||||
id: string | number
|
|
||||||
cid: string | number
|
|
||||||
reason: string | number
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function creator_authinfo_get(
|
|
||||||
params: RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function dj_difm_all_style_channel(
|
|
||||||
params: {
|
|
||||||
sources?: string
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function dj_difm_channel_subscribe(
|
|
||||||
params: {
|
|
||||||
id: string | number
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function dj_difm_channel_unsubscribe(
|
|
||||||
params: {
|
|
||||||
id: string | number
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function dj_difm_playing_tracks_list(
|
|
||||||
params: {
|
|
||||||
channelId: string | number
|
|
||||||
limit?: string | number
|
|
||||||
source?: string | number
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function dj_difm_subscribe_channels_get(
|
|
||||||
params: {
|
|
||||||
sources?: string
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function fanscenter_basicinfo_age_get(
|
|
||||||
params: RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function fanscenter_basicinfo_gender_get(
|
|
||||||
params: RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function fanscenter_basicinfo_province_get(
|
|
||||||
params: RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function fanscenter_overview_get(
|
|
||||||
params: RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function fanscenter_trend_list(
|
|
||||||
params: {
|
|
||||||
startTime?: string | number
|
|
||||||
endTime?: string | number
|
|
||||||
type?: string | number
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function lbs_city_code(
|
|
||||||
params: {
|
|
||||||
bizCode?: string
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function listen_data_realtime_report(
|
|
||||||
params: {
|
|
||||||
type?: string
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function listen_data_report(
|
|
||||||
params: {
|
|
||||||
type?: string
|
|
||||||
endTime?: string | number
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function listen_data_today_song(
|
|
||||||
params: RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function listen_data_total(params: RequestBaseConfig): Promise<Response>
|
|
||||||
|
|
||||||
export function listen_data_year_report(
|
|
||||||
params: RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function listentogether_accept(
|
|
||||||
params: {
|
|
||||||
roomId: string | number
|
|
||||||
inviterId: string | number
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function listentogether_end(
|
|
||||||
params: {
|
|
||||||
roomId: string | number
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function listentogether_heatbeat(
|
|
||||||
params: {
|
|
||||||
roomId: string | number
|
|
||||||
songId: string | number
|
|
||||||
playStatus: string | number
|
|
||||||
progress: string | number
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function listentogether_play_command(
|
|
||||||
params: {
|
|
||||||
roomId: string | number
|
|
||||||
commandType: string | number
|
|
||||||
progress?: string | number
|
|
||||||
playStatus: string | number
|
|
||||||
formerSongId: string | number
|
|
||||||
targetSongId: string | number
|
|
||||||
clientSeq: string | number
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function listentogether_room_check(
|
|
||||||
params: {
|
|
||||||
roomId: string | number
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function listentogether_room_create(
|
|
||||||
params: RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function listentogether_sync_list_command(
|
|
||||||
params: {
|
|
||||||
roomId: string | number
|
|
||||||
commandType: string | number
|
|
||||||
userId: string | number
|
|
||||||
version: string | number
|
|
||||||
randomList: string
|
|
||||||
displayList: string
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function listentogether_sync_playlist_get(
|
|
||||||
params: {
|
|
||||||
roomId: string | number
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function mlog_music_rcmd(
|
|
||||||
params: {
|
|
||||||
mvid?: string | number
|
|
||||||
songid?: string | number
|
|
||||||
limit?: string | number
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function music_first_listen_info(
|
|
||||||
params: {
|
|
||||||
id: string | number
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function personal_fm_mode(
|
|
||||||
params: {
|
|
||||||
mode: string
|
|
||||||
submode?: string
|
|
||||||
limit?: string | number
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function playlist_category_list(
|
|
||||||
params: {
|
|
||||||
cat?: string
|
|
||||||
limit?: string | number
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function playlist_detail_rcmd_get(
|
|
||||||
params: {
|
|
||||||
id: string | number
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function playlist_import_name_task_create(
|
|
||||||
params: {
|
|
||||||
local?: string
|
|
||||||
importStarPlaylist?: boolean
|
|
||||||
['name']?: string
|
|
||||||
['description']?: string
|
|
||||||
['id']?: string | number
|
|
||||||
['url']?: string
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function playlist_import_task_status(
|
|
||||||
params: {
|
|
||||||
id: string | number
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function playlist_privacy(
|
|
||||||
params: {
|
|
||||||
id: string | number
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function playmode_song_vector(
|
|
||||||
params: {
|
|
||||||
ids: string
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function radio_sport_get(
|
|
||||||
params: {
|
|
||||||
bpm?: string | number
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function recent_listen_list(params: RequestBaseConfig): Promise<Response>
|
|
||||||
|
|
||||||
export function recommend_songs_dislike(
|
|
||||||
params: {
|
|
||||||
id: string | number
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function register_anonimous(params: RequestBaseConfig): Promise<Response>
|
|
||||||
|
|
||||||
export function sati_resource_list(
|
|
||||||
params: {
|
|
||||||
tag: string
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function sati_resource_list_more(
|
|
||||||
params: {
|
|
||||||
id: string | number
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function sati_resource_sub(
|
|
||||||
params: {
|
|
||||||
id: string | number
|
|
||||||
cancel?: string | boolean
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function sati_resource_sub_list(
|
|
||||||
params: RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function sati_tag_list(params: RequestBaseConfig): Promise<Response>
|
|
||||||
|
|
||||||
export function sati_timescene_resources_get(
|
|
||||||
params: RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function search_match(
|
|
||||||
params: {
|
|
||||||
title?: string
|
|
||||||
album?: string
|
|
||||||
artist?: string
|
|
||||||
duration?: string | number
|
|
||||||
md5?: string
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function search_suggest_pc(
|
|
||||||
params: {
|
|
||||||
keyword: string
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function send_album(
|
|
||||||
params: {
|
|
||||||
id: string | number
|
|
||||||
msg?: string
|
|
||||||
user_ids: string
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function send_song(
|
|
||||||
params: {
|
|
||||||
id: string | number
|
|
||||||
msg?: string
|
|
||||||
user_ids: string
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function song_chorus(
|
|
||||||
params: {
|
|
||||||
id: string | number
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function song_creators(
|
|
||||||
params: {
|
|
||||||
id: string | number
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function song_downlist(
|
|
||||||
params: MultiPageConfig & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function song_download_url_v1(
|
|
||||||
params: {
|
|
||||||
id: string | number
|
|
||||||
level: SoundQualityType
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function song_dynamic_cover(
|
|
||||||
params: {
|
|
||||||
id: string | number
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function song_like(
|
|
||||||
params: {
|
|
||||||
id: string | number
|
|
||||||
like?: string | boolean
|
|
||||||
uid?: string | number
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function song_like_check(
|
|
||||||
params: {
|
|
||||||
ids: string
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function song_lyrics_mark(
|
|
||||||
params: {
|
|
||||||
id: string | number
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function song_lyrics_mark_add(
|
|
||||||
params: {
|
|
||||||
id: string | number
|
|
||||||
markId?: string
|
|
||||||
data?: string
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function song_lyrics_mark_del(
|
|
||||||
params: {
|
|
||||||
id: string | number
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function song_lyrics_mark_user_page(
|
|
||||||
params: MultiPageConfig & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function song_monthdownlist(
|
|
||||||
params: MultiPageConfig & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function song_music_detail(
|
|
||||||
params: {
|
|
||||||
id: string | number
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function song_red_count(
|
|
||||||
params: {
|
|
||||||
id: string | number
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function song_singledownlist(
|
|
||||||
params: MultiPageConfig & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function song_url_match(
|
|
||||||
params: {
|
|
||||||
id: string | number
|
|
||||||
source?: string
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function song_url_ncmget(params: RequestBaseConfig): Promise<Response>
|
|
||||||
|
|
||||||
export function song_url_v1_302(
|
|
||||||
params: {
|
|
||||||
id: string | number
|
|
||||||
level: SoundQualityType
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function starpick_comments_summary(
|
|
||||||
params: RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function summary_annual(
|
|
||||||
params: {
|
|
||||||
year: string
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function threshold_detail_get(
|
|
||||||
params: RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function toplist_detail_v2(params: RequestBaseConfig): Promise<Response>
|
|
||||||
|
|
||||||
export function ugc_album_get(
|
|
||||||
params: {
|
|
||||||
id: string | number
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function ugc_artist_get(
|
|
||||||
params: {
|
|
||||||
id: string | number
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function ugc_artist_search(
|
|
||||||
params: {
|
|
||||||
keyword: string
|
|
||||||
limit?: string | number
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function ugc_detail(
|
|
||||||
params: {
|
|
||||||
auditStatus?: string
|
|
||||||
type?: string | number
|
|
||||||
sortBy?: string
|
|
||||||
order?: string
|
|
||||||
} & MultiPageConfig &
|
|
||||||
RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function ugc_mv_get(
|
|
||||||
params: {
|
|
||||||
id: string | number
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function ugc_song_get(
|
|
||||||
params: {
|
|
||||||
id: string | number
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function ugc_user_devote(params: RequestBaseConfig): Promise<Response>
|
|
||||||
|
|
||||||
export function user_detail_new(
|
|
||||||
params: {
|
|
||||||
uid: string | number
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function user_follow_mixed(
|
|
||||||
params: {
|
|
||||||
size?: string | number
|
|
||||||
cursor?: string | number
|
|
||||||
scene?: string | number
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function user_medal(
|
|
||||||
params: {
|
|
||||||
uid: string | number
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function user_mutualfollow_get(
|
|
||||||
params: {
|
|
||||||
uid: string | number
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function user_playlist_collect(
|
|
||||||
params: {
|
|
||||||
uid: string | number
|
|
||||||
} & MultiPageConfig &
|
|
||||||
RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function user_playlist_create(
|
|
||||||
params: {
|
|
||||||
uid: string | number
|
|
||||||
} & MultiPageConfig &
|
|
||||||
RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function user_social_status(
|
|
||||||
params: {
|
|
||||||
uid: string | number
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function user_social_status_edit(
|
|
||||||
params: {
|
|
||||||
type: string | number
|
|
||||||
iconUrl?: string
|
|
||||||
content?: string
|
|
||||||
actionUrl?: string
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function user_social_status_rcmd(
|
|
||||||
params: RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function user_social_status_support(
|
|
||||||
params: RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function verify_getQr(
|
|
||||||
params: {
|
|
||||||
vid: string | number
|
|
||||||
type: string | number
|
|
||||||
token: string
|
|
||||||
evid: string
|
|
||||||
sign: string
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function verify_qrcodestatus(
|
|
||||||
params: {
|
|
||||||
qr: string
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function vip_sign(params: RequestBaseConfig): Promise<Response>
|
|
||||||
|
|
||||||
export function vip_sign_info(params: RequestBaseConfig): Promise<Response>
|
|
||||||
|
|
||||||
export function vip_tasks_v1(
|
|
||||||
params: {
|
|
||||||
id?: string | number
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function voice_detail(
|
|
||||||
params: {
|
|
||||||
id: string | number
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function voice_upload(
|
|
||||||
params: {
|
|
||||||
songFile: {
|
|
||||||
name: string
|
|
||||||
data: string | Buffer
|
|
||||||
}
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function voicelist_detail(
|
|
||||||
params: {
|
|
||||||
id: string | number
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function voicelist_list(
|
|
||||||
params: {
|
|
||||||
voiceListId: string | number
|
|
||||||
} & MultiPageConfig &
|
|
||||||
RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function voicelist_my_created(
|
|
||||||
params: {
|
|
||||||
limit?: string | number
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function voicelist_search(
|
|
||||||
params: {
|
|
||||||
keyword?: string
|
|
||||||
limit?: string | number
|
|
||||||
offset?: string | number
|
|
||||||
} & RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|
||||||
export function voicelist_trans(
|
|
||||||
params: {
|
|
||||||
radioId?: string | number
|
|
||||||
programId?: string | number
|
|
||||||
position?: string | number
|
|
||||||
} & MultiPageConfig &
|
|
||||||
RequestBaseConfig,
|
|
||||||
): Promise<Response>
|
|
||||||
|
|||||||
@ -1,97 +0,0 @@
|
|||||||
const {
|
|
||||||
eapiResDecrypt,
|
|
||||||
eapiReqDecrypt,
|
|
||||||
aesDecrypt,
|
|
||||||
xeapiResDecrypt,
|
|
||||||
} = require('../util/crypto')
|
|
||||||
const CryptoJS = require('crypto-js')
|
|
||||||
|
|
||||||
const linuxapiKey = 'rFgB&h#%2?^eDg:Q'
|
|
||||||
|
|
||||||
module.exports = async (query, request) => {
|
|
||||||
const crypto = query.crypto || 'eapi'
|
|
||||||
const data = query.data || query.hexString || ''
|
|
||||||
const isReq = query.isReq !== 'false'
|
|
||||||
|
|
||||||
if (!data) {
|
|
||||||
return {
|
|
||||||
status: 400,
|
|
||||||
body: { code: 400, message: 'data is required' },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
let result
|
|
||||||
switch (crypto) {
|
|
||||||
case 'eapi': {
|
|
||||||
const pureHex = data.replace(/\s/g, '')
|
|
||||||
result = isReq ? eapiReqDecrypt(pureHex) : eapiResDecrypt(pureHex)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'weapi': {
|
|
||||||
if (isReq) {
|
|
||||||
return {
|
|
||||||
status: 400,
|
|
||||||
body: {
|
|
||||||
code: 400,
|
|
||||||
message:
|
|
||||||
'weapi 请求解密需要 RSA 私钥,暂不支持;仅支持 weapi 返回数据解密(e_r=true 时与 eapi 相同)',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const pureHex = data.replace(/\s/g, '')
|
|
||||||
result = eapiResDecrypt(pureHex)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'linuxapi': {
|
|
||||||
if (isReq) {
|
|
||||||
const pureHex = data.replace(/\s/g, '')
|
|
||||||
const decrypted = aesDecrypt(pureHex, linuxapiKey, '', 'hex')
|
|
||||||
result = JSON.parse(decrypted.toString(CryptoJS.enc.Utf8))
|
|
||||||
} else {
|
|
||||||
result = typeof data === 'string' ? JSON.parse(data) : data
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'xeapi': {
|
|
||||||
if (isReq) {
|
|
||||||
return {
|
|
||||||
status: 400,
|
|
||||||
body: {
|
|
||||||
code: 400,
|
|
||||||
message:
|
|
||||||
'xeapi 请求解密涉及 X25519 ECDH 密钥交换,流程复杂,暂不支持;仅支持 xeapi 返回数据解密',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const buf = Buffer.from(data, 'base64')
|
|
||||||
result = xeapiResDecrypt(buf)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'api': {
|
|
||||||
result = typeof data === 'string' ? JSON.parse(data) : data
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
return {
|
|
||||||
status: 400,
|
|
||||||
body: { code: 400, message: `未知加密方式: ${crypto}` },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
status: 200,
|
|
||||||
body: { code: 200, data: result },
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
status: 400,
|
|
||||||
body: { code: 400, message: `解密失败: ${error.message}` },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,5 +1,4 @@
|
|||||||
// 私信和通知接口
|
// 私信和通知接口
|
||||||
|
|
||||||
const createOption = require('../util/option.js')
|
const createOption = require('../util/option.js')
|
||||||
module.exports = (query, request) => {
|
module.exports = (query, request) => {
|
||||||
const data = {}
|
const data = {}
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
// 获取游客cookie
|
|
||||||
|
|
||||||
const CryptoJS = require('crypto-js')
|
const CryptoJS = require('crypto-js')
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
@ -39,7 +37,7 @@ module.exports = async (query, request) => {
|
|||||||
let result = await request(
|
let result = await request(
|
||||||
`/api/register/anonimous`,
|
`/api/register/anonimous`,
|
||||||
data,
|
data,
|
||||||
createOption(query, 'xeapi'),
|
createOption(query, 'weapi'),
|
||||||
)
|
)
|
||||||
if (result.body.code === 200) {
|
if (result.body.code === 200) {
|
||||||
result = {
|
result = {
|
||||||
|
|||||||
@ -1,74 +0,0 @@
|
|||||||
const { default: axios } = require('axios')
|
|
||||||
const encrypt = require('../util/crypto')
|
|
||||||
const { APP_CONF } = require('../util/config.json')
|
|
||||||
|
|
||||||
const generateNonce = () => {
|
|
||||||
let nonce = ''
|
|
||||||
for (let i = 0; i < 16; i++) {
|
|
||||||
nonce += Math.floor(Math.random() * 10).toString()
|
|
||||||
}
|
|
||||||
return nonce
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = async (query, request) => {
|
|
||||||
const nonce = generateNonce()
|
|
||||||
const timestamp = String(Date.now())
|
|
||||||
const deviceId = query.deviceId || global.deviceId || ''
|
|
||||||
const currentKeyVersion = query.currentKeyVersion || ''
|
|
||||||
|
|
||||||
const data = {
|
|
||||||
appVersion: '9.1.65',
|
|
||||||
currentKeyVersion,
|
|
||||||
deviceId,
|
|
||||||
nonce,
|
|
||||||
os: 'android',
|
|
||||||
requestType: 'active',
|
|
||||||
signature: encrypt.xeapiSign(timestamp, nonce),
|
|
||||||
t1: '',
|
|
||||||
t2: '',
|
|
||||||
timestamp,
|
|
||||||
uid: '',
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await axios({
|
|
||||||
method: 'POST',
|
|
||||||
url: APP_CONF.apiDomain + '/api/gorilla/anti/crawler/security/key/get',
|
|
||||||
headers: {
|
|
||||||
'User-Agent':
|
|
||||||
'NeteaseMusic/9.1.65.240927161425(9001065);Dalvik/2.1.0 (Linux; U; Android 14; 23013RK75C Build/UKQ1.230804.001)',
|
|
||||||
Cookie: deviceId ? `deviceId=${encodeURIComponent(deviceId)}` : '',
|
|
||||||
},
|
|
||||||
data: new URLSearchParams(data).toString(),
|
|
||||||
proxy: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (
|
|
||||||
!res.data ||
|
|
||||||
res.data.code !== 200 ||
|
|
||||||
!res.data.data ||
|
|
||||||
!res.data.data.encryptedData
|
|
||||||
) {
|
|
||||||
throw new Error('xeapi public key request failed')
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
!res.data.data.signature ||
|
|
||||||
encrypt.xeapiSign(res.data.data.timestamp, nonce) !==
|
|
||||||
res.data.data.signature
|
|
||||||
) {
|
|
||||||
throw new Error('xeapi public key response signature mismatch')
|
|
||||||
}
|
|
||||||
|
|
||||||
const publicKey = encrypt.xeapiDecryptPublicKey(res.data.data.encryptedData)
|
|
||||||
if (!publicKey.sk) {
|
|
||||||
throw new Error('xeapi public key response missing sk')
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
status: 200,
|
|
||||||
body: {
|
|
||||||
...publicKey,
|
|
||||||
deviceId,
|
|
||||||
},
|
|
||||||
cookie: [],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,49 +0,0 @@
|
|||||||
// 提交歌曲播放状态
|
|
||||||
|
|
||||||
const createOption = require('../util/option.js')
|
|
||||||
const generateSessionId = () =>
|
|
||||||
Array.from(
|
|
||||||
{ length: 12 },
|
|
||||||
() =>
|
|
||||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'[Math.floor(Math.random() * 36)],
|
|
||||||
).join('')
|
|
||||||
|
|
||||||
module.exports = (query, request) => {
|
|
||||||
const {
|
|
||||||
id,
|
|
||||||
sessionId,
|
|
||||||
progress = 0,
|
|
||||||
playMode = 'list_loop',
|
|
||||||
type = 'song',
|
|
||||||
} = query
|
|
||||||
|
|
||||||
if (!id) {
|
|
||||||
return Promise.reject({
|
|
||||||
status: 400,
|
|
||||||
body: {
|
|
||||||
code: 400,
|
|
||||||
msg: '缺少必要参数:id',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const playStateSubmitReq = JSON.stringify({
|
|
||||||
resource: {
|
|
||||||
id: String(id),
|
|
||||||
type: type,
|
|
||||||
},
|
|
||||||
progress: Number(progress) || 0,
|
|
||||||
sessionId: sessionId || generateSessionId(),
|
|
||||||
playMode: playMode,
|
|
||||||
})
|
|
||||||
|
|
||||||
const data = {
|
|
||||||
playStateSubmitReq: playStateSubmitReq,
|
|
||||||
}
|
|
||||||
|
|
||||||
return request(
|
|
||||||
'/api/relay/play/state/submit',
|
|
||||||
data,
|
|
||||||
createOption(query, 'weapi'),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,42 +1,8 @@
|
|||||||
// 听歌打卡
|
// 听歌打卡
|
||||||
|
|
||||||
const createOption = require('../util/option.js')
|
const createOption = require('../util/option.js')
|
||||||
const { APP_CONF } = require('../util/config.json')
|
module.exports = (query, request) => {
|
||||||
const DOMAIN = APP_CONF.clDomian
|
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([
|
logs: JSON.stringify([
|
||||||
{
|
{
|
||||||
action: 'play',
|
action: 'play',
|
||||||
@ -49,30 +15,12 @@ module.exports = async (query, request) => {
|
|||||||
type: 'song',
|
type: 'song',
|
||||||
wifi: 0,
|
wifi: 0,
|
||||||
source: 'list',
|
source: 'list',
|
||||||
mainsite: '1',
|
mainsite: 1,
|
||||||
mainsiteWeb: '1',
|
content: '',
|
||||||
content: `id=${query.sourceid}`,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
}
|
}
|
||||||
|
|
||||||
const option = createOption(query, 'eapi')
|
return request(`/api/feedback/weblog`, data, createOption(query, 'weapi'))
|
||||||
option.domain = DOMAIN
|
|
||||||
|
|
||||||
// 发送两次请求
|
|
||||||
const res1 = await request(`/api/feedback/weblog`, startplayData, option)
|
|
||||||
const res2 = await request(`/api/feedback/weblog`, playData, option)
|
|
||||||
|
|
||||||
return {
|
|
||||||
status: 200,
|
|
||||||
body: {
|
|
||||||
code: 200,
|
|
||||||
data: 'success',
|
|
||||||
details: {
|
|
||||||
startplay: res1.body,
|
|
||||||
play: res2.body,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,128 +0,0 @@
|
|||||||
// 听歌打卡 - NCBL 加密版 (仿桌面客户端 PLV/PLD 上报)
|
|
||||||
// 复制自 https://github.com/folltoshe/netease-report-listen-song 的 PC 端日志上报方式
|
|
||||||
//
|
|
||||||
// PLV 和 PLD 分两次独立上传
|
|
||||||
|
|
||||||
const {
|
|
||||||
buildPlv,
|
|
||||||
buildPld,
|
|
||||||
buildRecords,
|
|
||||||
extractContext,
|
|
||||||
parseCookie,
|
|
||||||
buildCookieStr,
|
|
||||||
buildMetaJson,
|
|
||||||
doUpload,
|
|
||||||
} = require('../util/ncbl')
|
|
||||||
|
|
||||||
module.exports = async (query, request) => {
|
|
||||||
// --- 参数校验 ---
|
|
||||||
const songId = Number(query.id)
|
|
||||||
if (!songId || isNaN(songId)) {
|
|
||||||
return { status: 400, body: { code: 400, msg: '缺少有效的 id (歌曲ID)' } }
|
|
||||||
}
|
|
||||||
const playTime = Number(query.time)
|
|
||||||
if (isNaN(playTime) || playTime <= 0) {
|
|
||||||
return {
|
|
||||||
status: 400,
|
|
||||||
body: { code: 400, msg: '缺少有效的 time (播放时长)' },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const totalTime = Number(query.total) || playTime
|
|
||||||
const sourceId = String(query.sourceid || query.sourceId || '')
|
|
||||||
const sourceName = query.source || 'list'
|
|
||||||
|
|
||||||
// --- 解析认证上下文 ---
|
|
||||||
const rawCookie = query.cookie || ''
|
|
||||||
const cookieObj = parseCookie(rawCookie)
|
|
||||||
cookieObj.os = 'pc'
|
|
||||||
const ctx = extractContext(cookieObj)
|
|
||||||
|
|
||||||
// 兜底取 token
|
|
||||||
if (!ctx.auth.token && rawCookie) {
|
|
||||||
const parsed =
|
|
||||||
typeof rawCookie === 'string' ? parseCookie(rawCookie) : rawCookie
|
|
||||||
ctx.auth.token = parsed.MUSIC_U || ''
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ctx.auth.token) {
|
|
||||||
return { status: 401, body: { code: 401, msg: '缺少 MUSIC_U 鉴权令牌' } }
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- 构建歌曲和来源 ---
|
|
||||||
const song = {
|
|
||||||
id: songId,
|
|
||||||
name: query.name || '',
|
|
||||||
artist: query.artist || '',
|
|
||||||
bitrate: Number(query.bitrate) || 320,
|
|
||||||
level: query.level || 'exhigh',
|
|
||||||
vip: query.vip === 'true' || query.vip === true,
|
|
||||||
time: totalTime,
|
|
||||||
}
|
|
||||||
const source = {
|
|
||||||
id: sourceId || String(songId),
|
|
||||||
type: 'track',
|
|
||||||
name: sourceName,
|
|
||||||
}
|
|
||||||
|
|
||||||
const metaJson = buildMetaJson(ctx)
|
|
||||||
const cookieStr = buildCookieStr(ctx)
|
|
||||||
|
|
||||||
const ts = Math.floor(Date.now() / 1000)
|
|
||||||
const played = Math.min(playTime, totalTime)
|
|
||||||
|
|
||||||
const plvBody = buildRecords([
|
|
||||||
{ time: ts, action: '_plv', data: buildPlv(ctx, song, source) },
|
|
||||||
])
|
|
||||||
const pldBody = buildRecords([
|
|
||||||
{ time: ts, action: '_pld', data: buildPld(ctx, song, source, played) },
|
|
||||||
])
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 1) 上传 PLV
|
|
||||||
const plv = await doUpload(ctx, metaJson, plvBody, cookieStr, 'PLV')
|
|
||||||
if (!plv.success) {
|
|
||||||
const rateMsg =
|
|
||||||
plv.respBody?.data?.rate != null
|
|
||||||
? ` (rate=${plv.respBody.data.rate})`
|
|
||||||
: ''
|
|
||||||
return {
|
|
||||||
status: 200,
|
|
||||||
body: {
|
|
||||||
code: plv.respBody?.code || -1,
|
|
||||||
msg: `PLV 上报失败${rateMsg}`,
|
|
||||||
details: plv.respBody,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2) 上传 PLD
|
|
||||||
const pld = await doUpload(ctx, metaJson, pldBody, cookieStr, 'PLD')
|
|
||||||
if (!pld.success) {
|
|
||||||
return {
|
|
||||||
status: 200,
|
|
||||||
body: {
|
|
||||||
code: pld.respBody?.code || -1,
|
|
||||||
msg: 'PLV 成功但 PLD 失败',
|
|
||||||
details: { plv: plv.respBody, pld: pld.respBody },
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
status: 200,
|
|
||||||
body: {
|
|
||||||
code: 200,
|
|
||||||
data: 'scrobble_v1 上报成功',
|
|
||||||
details: {
|
|
||||||
plv: { fileName: plv.fileName, payloadSize: plv.payload.length },
|
|
||||||
pld: { fileName: pld.fileName, payloadSize: pld.payload.length },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
return {
|
|
||||||
status: 502,
|
|
||||||
body: { code: 502, msg: `请求异常: ${err.message || err}` },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
// 从云盘获取歌曲下载链接
|
|
||||||
|
|
||||||
const createOption = require('../util/option.js')
|
|
||||||
module.exports = (query, request) => {
|
|
||||||
const data = {
|
|
||||||
songId: query.id,
|
|
||||||
}
|
|
||||||
return request(`/api/cloud/dowonload`, data, createOption(query, 'eapi'))
|
|
||||||
}
|
|
||||||
@ -53,9 +53,5 @@ module.exports = async (query, request) => {
|
|||||||
if (data.level == 'sky') {
|
if (data.level == 'sky') {
|
||||||
data.immerseType = 'c51'
|
data.immerseType = 'c51'
|
||||||
}
|
}
|
||||||
return request(
|
return request(`/api/song/enhance/player/url/v1`, data, createOption(query))
|
||||||
`/api/song/enhance/player/url/v1`,
|
|
||||||
data,
|
|
||||||
createOption(query, 'xeapi'),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,9 +3,5 @@
|
|||||||
const createOption = require('../util/option.js')
|
const createOption = require('../util/option.js')
|
||||||
module.exports = (query, request) => {
|
module.exports = (query, request) => {
|
||||||
const data = {}
|
const data = {}
|
||||||
return request(
|
return request(`/api/vip-center-bff/task/sign`, data, createOption(query))
|
||||||
`/api/vip-center-bff/task/sign`,
|
|
||||||
data,
|
|
||||||
createOption(query, 'weapi'),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,14 +0,0 @@
|
|||||||
// 黑胶乐签打卡详情
|
|
||||||
|
|
||||||
const createOption = require('../util/option.js')
|
|
||||||
module.exports = (query, request) => {
|
|
||||||
const data = {
|
|
||||||
signDayTime: query.timestamp,
|
|
||||||
type: '1',
|
|
||||||
}
|
|
||||||
return request(
|
|
||||||
`/api/vipnewcenter/app/level/user/checkin/history/detail`,
|
|
||||||
data,
|
|
||||||
createOption(query, 'eapi'),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
// 黑胶乐签打卡历史
|
|
||||||
|
|
||||||
const createOption = require('../util/option.js')
|
|
||||||
module.exports = (query, request) => {
|
|
||||||
const data = {
|
|
||||||
type: '0',
|
|
||||||
}
|
|
||||||
return request(
|
|
||||||
`/api/vipnewcenter/app/minidesk/music/sign/pc`,
|
|
||||||
data,
|
|
||||||
createOption(query, 'xeapi'),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
// 黑胶乐签未来签到信息
|
// 黑胶乐签签到信息
|
||||||
|
|
||||||
const createOption = require('../util/option.js')
|
const createOption = require('../util/option.js')
|
||||||
module.exports = (query, request) => {
|
module.exports = (query, request) => {
|
||||||
|
|||||||
@ -1,14 +0,0 @@
|
|||||||
// 会员任务 - 新版
|
|
||||||
|
|
||||||
const createOption = require('../util/option.js')
|
|
||||||
module.exports = (query, request) => {
|
|
||||||
const data = {
|
|
||||||
taskType: 'app_vip_task_center',
|
|
||||||
userId: query.id,
|
|
||||||
}
|
|
||||||
return request(
|
|
||||||
`/api/middle/vip/mission/user/progress/list`,
|
|
||||||
data,
|
|
||||||
createOption(query, 'xeapi'),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
32
package.json
32
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@neteasecloudmusicapienhanced/api",
|
"name": "@neteasecloudmusicapienhanced/api",
|
||||||
"version": "4.36.1",
|
"version": "4.33.1",
|
||||||
"description": "全网最全的网易云音乐API接口 || A revival project for NeteaseCloudMusicApi Node.js Services (Half Refactor & Enhanced) || 网易云音乐 API 备份 + 增强 || 本项目自原版v4.28.0版本后开始自行维护",
|
"description": "全网最全的网易云音乐API接口 || A revival project for NeteaseCloudMusicApi Node.js Services (Half Refactor & Enhanced) || 网易云音乐 API 备份 + 增强 || 本项目自原版v4.28.0版本后开始自行维护",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "nodemon app.js",
|
"dev": "nodemon app.js",
|
||||||
@ -9,8 +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",
|
|
||||||
"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"
|
||||||
@ -43,14 +41,6 @@
|
|||||||
],
|
],
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"types": "./interface.d.ts",
|
"types": "./interface.d.ts",
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "git+https://github.com/NeteaseCloudMusicApiEnhanced/api-enhanced.git"
|
|
||||||
},
|
|
||||||
"bugs": {
|
|
||||||
"url": "https://github.com/NeteaseCloudMusicApiEnhanced/api-enhanced/issues"
|
|
||||||
},
|
|
||||||
"homepage": "https://neteasecloudmusicapienhanced.js.org/",
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@ -75,14 +65,14 @@
|
|||||||
"data"
|
"data"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@neteasecloudmusicapienhanced/unblockmusic-utils": "^0.3.3",
|
"@neteasecloudmusicapienhanced/unblockmusic-utils": "^0.3.1",
|
||||||
"axios": "^1.18.0",
|
"axios": "^1.16.1",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"dotenv": "^17.4.2",
|
"dotenv": "^17.4.2",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
"express-fileupload": "^1.5.2",
|
"express-fileupload": "^1.5.2",
|
||||||
"gzip": "^0.1.0",
|
"gzip": "^0.1.0",
|
||||||
"music-metadata": "^11.13.0",
|
"music-metadata": "^11.12.3",
|
||||||
"node-forge": "^1.4.0",
|
"node-forge": "^1.4.0",
|
||||||
"pac-proxy-agent": "^7.2.0",
|
"pac-proxy-agent": "^7.2.0",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
@ -97,13 +87,13 @@
|
|||||||
"@types/express": "^5.0.6",
|
"@types/express": "^5.0.6",
|
||||||
"@types/express-fileupload": "^1.5.1",
|
"@types/express-fileupload": "^1.5.1",
|
||||||
"@types/mocha": "^10.0.10",
|
"@types/mocha": "^10.0.10",
|
||||||
"@types/node": "25.9.2",
|
"@types/node": "25.5.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.61.1",
|
"@typescript-eslint/eslint-plugin": "^8.59.4",
|
||||||
"@typescript-eslint/parser": "^8.61.1",
|
"@typescript-eslint/parser": "^8.59.4",
|
||||||
"eslint": "^9.39.4",
|
"eslint": "^9.39.4",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"eslint-plugin-html": "^8.1.4",
|
"eslint-plugin-html": "^8.1.4",
|
||||||
"eslint-plugin-prettier": "^5.5.6",
|
"eslint-plugin-prettier": "^5.5.5",
|
||||||
"globals": "^17.6.0",
|
"globals": "^17.6.0",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"intelli-espower-loader": "^1.1.0",
|
"intelli-espower-loader": "^1.1.0",
|
||||||
@ -112,11 +102,7 @@
|
|||||||
"nodemon": "^3.1.14",
|
"nodemon": "^3.1.14",
|
||||||
"pkg": "^5.8.1",
|
"pkg": "^5.8.1",
|
||||||
"power-assert": "^1.6.1",
|
"power-assert": "^1.6.1",
|
||||||
"prettier": "^3.8.4",
|
"prettier": "^3.8.3",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "^5.9.3"
|
||||||
},
|
|
||||||
"publishConfig": {
|
|
||||||
"access": "public",
|
|
||||||
"registry": "https://registry.npmjs.org/"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
493
pnpm-lock.yaml
generated
493
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -132,7 +132,6 @@
|
|||||||
<option value="eapi">eapi</option>
|
<option value="eapi">eapi</option>
|
||||||
<option value="api">api</option>
|
<option value="api">api</option>
|
||||||
<option value="linuxapi">linuxapi</option>
|
<option value="linuxapi">linuxapi</option>
|
||||||
<option value="xeapi">xeapi</option>
|
|
||||||
<option value="" selected>(默认)</option>
|
<option value="" selected>(默认)</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,695 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="zh">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>API 参数和返回内容解析</title>
|
|
||||||
<style>
|
|
||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
||||||
min-height: 100vh;
|
|
||||||
background: #f5f5f5;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
max-width: 900px;
|
|
||||||
margin: 0 auto;
|
|
||||||
background: white;
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 32px;
|
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 24px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #333;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group {
|
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
|
||||||
display: block;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #555;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
textarea {
|
|
||||||
width: 100%;
|
|
||||||
padding: 12px;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-family: 'Courier New', monospace;
|
|
||||||
font-size: 13px;
|
|
||||||
resize: vertical;
|
|
||||||
min-height: 200px;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
textarea:focus {
|
|
||||||
border-color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.radio-group {
|
|
||||||
display: flex;
|
|
||||||
gap: 24px;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.radio-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.radio-item input[type="radio"] {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.radio-item label {
|
|
||||||
margin: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
background: #333;
|
|
||||||
color: white;
|
|
||||||
padding: 12px 28px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 500;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
button + button {
|
|
||||||
margin-left: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:hover {
|
|
||||||
background: #555;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result-section {
|
|
||||||
margin-top: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result-section label {
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.decode-result {
|
|
||||||
white-space: pre-wrap;
|
|
||||||
word-break: break-all;
|
|
||||||
background: #f9f9f9;
|
|
||||||
padding: 16px;
|
|
||||||
border-radius: 6px;
|
|
||||||
border: 1px solid #eee;
|
|
||||||
min-height: 200px;
|
|
||||||
max-height: 400px;
|
|
||||||
overflow: auto;
|
|
||||||
font-family: 'Courier New', monospace;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.decode-result .json-key { color: #881391; }
|
|
||||||
.decode-result .json-string { color: #1a7f37; }
|
|
||||||
.decode-result .json-number { color: #0550ae; }
|
|
||||||
.decode-result .json-boolean { color: #cf222e; }
|
|
||||||
.decode-result .json-null { color: #656d76; }
|
|
||||||
|
|
||||||
.example-section {
|
|
||||||
margin-top: 32px;
|
|
||||||
padding-top: 24px;
|
|
||||||
border-top: 1px solid #eee;
|
|
||||||
}
|
|
||||||
|
|
||||||
.example-section h2 {
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #333;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.example-section img {
|
|
||||||
max-width: 100%;
|
|
||||||
height: auto;
|
|
||||||
border-radius: 6px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
border: 1px solid #eee;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* new elements for multi-crypto support */
|
|
||||||
.crypto-bar {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
padding: 12px 16px;
|
|
||||||
background: #fafafa;
|
|
||||||
border-radius: 6px;
|
|
||||||
border: 1px solid #eee;
|
|
||||||
}
|
|
||||||
|
|
||||||
.crypto-bar .bar-label {
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #555;
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.crypto-btn {
|
|
||||||
padding: 5px 14px;
|
|
||||||
border-radius: 4px;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
background: #fff;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #666;
|
|
||||||
transition: all 0.15s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.crypto-btn:hover {
|
|
||||||
border-color: #333;
|
|
||||||
color: #333;
|
|
||||||
background: #f5f5f5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.crypto-btn.active {
|
|
||||||
border-color: #333;
|
|
||||||
background: #333;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.crypto-btn .badge {
|
|
||||||
display: inline-block;
|
|
||||||
font-size: 10px;
|
|
||||||
background: rgba(0,0,0,0.08);
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 0 5px;
|
|
||||||
margin-left: 3px;
|
|
||||||
}
|
|
||||||
.crypto-btn.active .badge {
|
|
||||||
background: rgba(255,255,255,0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mode-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 20px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mode-row .mode-label {
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #555;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mode-row .radio-group {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-hint {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #888;
|
|
||||||
margin-top: 6px;
|
|
||||||
padding: 6px 10px;
|
|
||||||
background: #fafafa;
|
|
||||||
border-radius: 4px;
|
|
||||||
border-left: 3px solid #999;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-hint.warn {
|
|
||||||
border-left-color: #e67e22;
|
|
||||||
background: #fef9f0;
|
|
||||||
color: #d35400;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-row {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-row button + button {
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
button.btn-secondary {
|
|
||||||
background: #e8e8e8;
|
|
||||||
color: #444;
|
|
||||||
}
|
|
||||||
button.btn-secondary:hover {
|
|
||||||
background: #d5d5d5;
|
|
||||||
}
|
|
||||||
button.btn-secondary:disabled {
|
|
||||||
opacity: 0.4;
|
|
||||||
cursor: not-allowed;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
button.btn-success {
|
|
||||||
background: #4caf50;
|
|
||||||
}
|
|
||||||
button.btn-success:hover {
|
|
||||||
background: #43a047;
|
|
||||||
}
|
|
||||||
button.btn-success:disabled {
|
|
||||||
opacity: 0.4;
|
|
||||||
cursor: not-allowed;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result-section .section-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result-section .section-header label {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.copy-btn {
|
|
||||||
font-size: 12px;
|
|
||||||
padding: 3px 12px;
|
|
||||||
border-radius: 4px;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
background: #fff;
|
|
||||||
cursor: pointer;
|
|
||||||
color: #666;
|
|
||||||
transition: all 0.15s;
|
|
||||||
}
|
|
||||||
.copy-btn:hover {
|
|
||||||
background: #333;
|
|
||||||
color: #fff;
|
|
||||||
border-color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.example-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
gap: 16px;
|
|
||||||
margin-top: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.example-card {
|
|
||||||
background: #fafafa;
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 16px;
|
|
||||||
border: 1px solid #eee;
|
|
||||||
}
|
|
||||||
|
|
||||||
.example-card h3 {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #555;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.example-card pre {
|
|
||||||
font-family: 'Courier New', monospace;
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 1.5;
|
|
||||||
background: #fff;
|
|
||||||
padding: 10px;
|
|
||||||
border-radius: 4px;
|
|
||||||
overflow-x: auto;
|
|
||||||
border: 1px solid #e8e8e8;
|
|
||||||
max-height: 120px;
|
|
||||||
overflow-y: auto;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.example-card .tag {
|
|
||||||
display: inline-block;
|
|
||||||
font-size: 10px;
|
|
||||||
font-weight: 600;
|
|
||||||
padding: 2px 8px;
|
|
||||||
border-radius: 3px;
|
|
||||||
margin-bottom: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tag-eapi { background: #e3f2fd; color: #1565c0; }
|
|
||||||
.tag-weapi { background: #fce4ec; color: #c62828; }
|
|
||||||
.tag-linuxapi { background: #e8f5e9; color: #2e7d32; }
|
|
||||||
.tag-xeapi { background: #f3e5f5; color: #7b1fa2; }
|
|
||||||
.tag-api { background: #fff3e0; color: #e65100; }
|
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
|
||||||
.example-grid { grid-template-columns: 1fr; }
|
|
||||||
.crypto-bar { gap: 4px; }
|
|
||||||
.crypto-btn { padding: 4px 10px; font-size: 12px; }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<div id="app" class="container">
|
|
||||||
<h1>API 参数和返回内容解析</h1>
|
|
||||||
|
|
||||||
<!-- 加密方式选择 -->
|
|
||||||
<div class="crypto-bar">
|
|
||||||
<span class="bar-label">加密方式</span>
|
|
||||||
<button
|
|
||||||
v-for="c in cryptoList"
|
|
||||||
:key="c.value"
|
|
||||||
class="crypto-btn"
|
|
||||||
:class="{ active: crypto === c.value }"
|
|
||||||
@click="selectCrypto(c.value)"
|
|
||||||
>
|
|
||||||
{{ c.label }}
|
|
||||||
<span class="badge">{{ c.badge }}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mode-row">
|
|
||||||
<span class="mode-label">数据类型</span>
|
|
||||||
<div class="radio-group">
|
|
||||||
<div class="radio-item">
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
id="mode-req"
|
|
||||||
:value="true"
|
|
||||||
v-model="isReq"
|
|
||||||
:disabled="cryptoInfo.reqDisabled"
|
|
||||||
/>
|
|
||||||
<label for="mode-req">请求数据 request</label>
|
|
||||||
</div>
|
|
||||||
<div class="radio-item">
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
id="mode-resp"
|
|
||||||
:value="false"
|
|
||||||
v-model="isReq"
|
|
||||||
:disabled="cryptoInfo.respDisabled"
|
|
||||||
/>
|
|
||||||
<label for="mode-resp">返回数据 response</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="dataInput">{{ cryptoInfo.inputLabel }}</label>
|
|
||||||
<textarea
|
|
||||||
id="dataInput"
|
|
||||||
v-model="encryptedData"
|
|
||||||
:rows="crypto === 'xeapi' ? 8 : 10"
|
|
||||||
:placeholder="cryptoInfo.placeholder"
|
|
||||||
></textarea>
|
|
||||||
<div class="info-hint" :class="{ warn: cryptoInfo.warning }">
|
|
||||||
{{ cryptoInfo.hint }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="action-row">
|
|
||||||
<button @click="decrypt">解密</button>
|
|
||||||
<button class="btn-success" @click="sendToApi" :disabled="!canSend">
|
|
||||||
填入 API 调试
|
|
||||||
</button>
|
|
||||||
<button class="btn-secondary" @click="loadExample">加载示例</button>
|
|
||||||
<button class="btn-secondary" @click="clearAll">清空</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="result-section">
|
|
||||||
<div class="section-header">
|
|
||||||
<label>解密结果:</label>
|
|
||||||
<button
|
|
||||||
v-if="result && result !== '{}' && result !== 'null'"
|
|
||||||
class="copy-btn"
|
|
||||||
@click="copyResult"
|
|
||||||
>
|
|
||||||
复制
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<pre class="decode-result" v-html="highlightedResult"></pre>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="example-section">
|
|
||||||
<h2>使用示例</h2>
|
|
||||||
<img src="/static/eapi_params.png" alt="请求示例" />
|
|
||||||
<img src="/static/eapi_response.png" alt="响应示例" />
|
|
||||||
|
|
||||||
<div class="example-grid">
|
|
||||||
<div class="example-card">
|
|
||||||
<span class="tag tag-eapi">eapi</span>
|
|
||||||
<h3>请求参数解密</h3>
|
|
||||||
<pre>输入 hex 字符串,解密后得到请求 URL 和参数</pre>
|
|
||||||
<button class="copy-btn" @click="loadExampleData('eapi_req')">加载示例</button>
|
|
||||||
</div>
|
|
||||||
<div class="example-card">
|
|
||||||
<span class="tag tag-linuxapi">linuxapi</span>
|
|
||||||
<h3>请求 eparams 解密</h3>
|
|
||||||
<pre>AES-ECB 解密 linuxapi 的 eparams 参数(hex)</pre>
|
|
||||||
<button class="copy-btn" @click="loadExampleData('linuxapi_req')">加载示例</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="https://fastly.jsdelivr.net/npm/axios"></script>
|
|
||||||
<script src="https://fastly.jsdelivr.net/npm/vue@3"></script>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
const cryptoInfos = {
|
|
||||||
eapi: {
|
|
||||||
label: 'eapi',
|
|
||||||
badge: 'AES-ECB',
|
|
||||||
inputLabel: '十六进制字符串(Hex)',
|
|
||||||
placeholder: '粘贴 eapi 加密后的 Hex 字符串...',
|
|
||||||
hint: 'eapi 使用 AES-ECB 加密,密钥 e82ckenh8dichen8。请求格式: url-36cd479b6b5-data-36cd479b6b5-md5',
|
|
||||||
warning: false,
|
|
||||||
reqDisabled: false,
|
|
||||||
respDisabled: false,
|
|
||||||
},
|
|
||||||
weapi: {
|
|
||||||
label: 'weapi',
|
|
||||||
badge: 'AES-CBC+RSA',
|
|
||||||
inputLabel: '十六进制字符串(Hex)',
|
|
||||||
placeholder: '粘贴 weapi 返回数据的 Hex 字符串(e_r=true 时)...',
|
|
||||||
hint: 'weapi 请求解密需要 RSA 私钥,暂不支持。仅支持返回数据解密(同 eapi 的 AES-ECB)',
|
|
||||||
warning: true,
|
|
||||||
reqDisabled: true,
|
|
||||||
respDisabled: false,
|
|
||||||
},
|
|
||||||
linuxapi: {
|
|
||||||
label: 'linuxapi',
|
|
||||||
badge: 'AES-ECB',
|
|
||||||
inputLabel: '十六进制字符串(Hex)',
|
|
||||||
placeholder: '粘贴 linuxapi 的 eparams (Hex 格式)...',
|
|
||||||
hint: 'linuxapi 请求 eparams 使用 AES-ECB 加密,密钥 rFgB&h#%2?^eDg:Q。返回为明文 JSON',
|
|
||||||
warning: false,
|
|
||||||
reqDisabled: false,
|
|
||||||
respDisabled: false,
|
|
||||||
},
|
|
||||||
xeapi: {
|
|
||||||
label: 'xeapi',
|
|
||||||
badge: 'X25519+AES',
|
|
||||||
inputLabel: 'Base64 编码字符串',
|
|
||||||
placeholder: '粘贴 xeapi 返回数据的 Base64 原始二进制...',
|
|
||||||
hint: 'xeapi 请求涉及 X25519 ECDH 密钥交换,暂不支持。返回数据使用 AES-ECB + eapiKey 加密,可能带 gzip',
|
|
||||||
warning: true,
|
|
||||||
reqDisabled: true,
|
|
||||||
respDisabled: false,
|
|
||||||
},
|
|
||||||
api: {
|
|
||||||
label: 'api',
|
|
||||||
badge: '明文',
|
|
||||||
inputLabel: 'JSON 文本',
|
|
||||||
placeholder: '粘贴 api 的请求或返回 JSON...',
|
|
||||||
hint: 'api(明文)不经加密,直接传递 JSON 数据',
|
|
||||||
warning: false,
|
|
||||||
reqDisabled: false,
|
|
||||||
respDisabled: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const examples = {
|
|
||||||
eapi_req:
|
|
||||||
'AD96DDB984491E79B6F429DD650C6E2AE524627AC223AC9A123C66BB0997965950FED137544A93DFC718E16F57C8C121AF537086F395570A5602A3922366D11964DAFACD7830AACABF62E5650E67F457E79C1D2E13502391FC3487216CC5BF8681843FCB8E05559487EB18AAC1BE0EFEA4F7B6A050478366153A9426C238B8869600B275704555A9EB94C92E4F3FDABE9E0BCE07645410D0AA7B675698A4CAE6CD3620633ABF0B849A4244CC8DFC5DB2646D5EA9B3954E62BFEF19AFEAFDDC34E55C3E9A1DD3167CF53D443617108141',
|
|
||||||
linuxapi_req:
|
|
||||||
'A0D9583F4C5FF68DE851D2893A49DE98CC059A8845B664AA2459CEA7271A2C0E5BCA8A188E1BE398DB4C9A3FC117E19C9BAC491F454D17D403C2389476AB0FF4296B00294AD1EBDA141C188DF918F6B9599DAA5739928FD52B4AE580D8657903CE3C6633D2E46AD242408AE219B8191E',
|
|
||||||
}
|
|
||||||
|
|
||||||
const app = Vue.createApp({
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
crypto: 'eapi',
|
|
||||||
encryptedData: '',
|
|
||||||
result: '{}',
|
|
||||||
isReq: true,
|
|
||||||
cryptoList: [
|
|
||||||
{ value: 'eapi', label: 'eapi', badge: 'AES-ECB' },
|
|
||||||
{ value: 'weapi', label: 'weapi', badge: 'AES-CBC+RSA' },
|
|
||||||
{ value: 'linuxapi', label: 'linuxapi', badge: 'AES-ECB' },
|
|
||||||
{ value: 'xeapi', label: 'xeapi', badge: 'X25519+AES' },
|
|
||||||
{ value: 'api', label: 'api', badge: '明文' },
|
|
||||||
],
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.loadExample()
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
cryptoInfo() {
|
|
||||||
return cryptoInfos[this.crypto] || cryptoInfos.eapi
|
|
||||||
},
|
|
||||||
isRequestMode() {
|
|
||||||
return this.isReq === true || this.isReq === 'true'
|
|
||||||
},
|
|
||||||
canSend() {
|
|
||||||
if (!this.isRequestMode) return false
|
|
||||||
if (!this.result || this.result === '{}' || this.result === 'null') return false
|
|
||||||
try {
|
|
||||||
JSON.parse(this.result)
|
|
||||||
return true
|
|
||||||
} catch (error) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
highlightedResult() {
|
|
||||||
if (!this.result || this.result === '') return ''
|
|
||||||
try {
|
|
||||||
const parsed = typeof this.result === 'string' ? JSON.parse(this.result) : this.result
|
|
||||||
const json = JSON.stringify(parsed, null, 2)
|
|
||||||
return this.syntaxHighlight(json)
|
|
||||||
} catch (error) {
|
|
||||||
return this.escapeHtml(String(this.result))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
crypto(val) {
|
|
||||||
const info = cryptoInfos[val]
|
|
||||||
if (this.isReq && info.reqDisabled) {
|
|
||||||
this.isReq = false
|
|
||||||
} else if (!this.isReq && info.respDisabled) {
|
|
||||||
this.isReq = true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
selectCrypto(val) {
|
|
||||||
this.crypto = val
|
|
||||||
},
|
|
||||||
syntaxHighlight(json) {
|
|
||||||
const escaped = this.escapeHtml(json)
|
|
||||||
return escaped
|
|
||||||
.replace(/(?:"(?:\\.|[^"\\])*")\s*:/g, '<span class="json-key">$&</span>')
|
|
||||||
.replace(/:(\s*)(?:"(?:\\.|[^"\\])*")/g, ':<span class="json-string">$1$2</span>')
|
|
||||||
.replace(/:\s*(\d+(?:\.\d+)?)/g, ': <span class="json-number">$1</span>')
|
|
||||||
.replace(/:\s*(true|false)/g, ': <span class="json-boolean">$1</span>')
|
|
||||||
.replace(/:\s*(null)/g, ': <span class="json-null">$1</span>')
|
|
||||||
},
|
|
||||||
escapeHtml(text) {
|
|
||||||
const div = document.createElement('div')
|
|
||||||
div.textContent = text
|
|
||||||
return div.innerHTML
|
|
||||||
},
|
|
||||||
formatResult(value) {
|
|
||||||
if (value == null || value === '') return ''
|
|
||||||
try {
|
|
||||||
const parsed = typeof value === 'string' ? JSON.parse(value) : value
|
|
||||||
return JSON.stringify(parsed, null, 2)
|
|
||||||
} catch (error) {
|
|
||||||
return String(value)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async decrypt() {
|
|
||||||
if (!this.encryptedData || !this.encryptedData.trim()) {
|
|
||||||
alert('请先输入要解密的数据')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const res = await axios({
|
|
||||||
url: `/decrypt?crypto=${this.crypto}&isReq=${this.isReq}×tamp=${Date.now()}`,
|
|
||||||
method: 'post',
|
|
||||||
data: { data: this.encryptedData },
|
|
||||||
})
|
|
||||||
this.result = JSON.stringify(res.data.data)
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
const msg = error?.response?.data?.message || error?.message || '解密失败,数据格式错误'
|
|
||||||
alert(msg)
|
|
||||||
this.result = '{}'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
sendToApi() {
|
|
||||||
if (!this.canSend) return
|
|
||||||
const payload = JSON.parse(this.result)
|
|
||||||
const params = new URLSearchParams()
|
|
||||||
const uri = payload.uri || payload.url || payload.path || ''
|
|
||||||
params.set('uri', uri)
|
|
||||||
params.set('crypto', this.crypto)
|
|
||||||
const data = payload.params || payload.data || payload.body || payload.payload || payload.request || {}
|
|
||||||
params.set('data', JSON.stringify(data))
|
|
||||||
window.open(`/api.html?${params.toString()}`, '_blank')
|
|
||||||
},
|
|
||||||
loadExample() {
|
|
||||||
switch (this.crypto) {
|
|
||||||
case 'eapi':
|
|
||||||
this.encryptedData = examples.eapi_req
|
|
||||||
break
|
|
||||||
case 'linuxapi':
|
|
||||||
this.isReq = true
|
|
||||||
this.encryptedData = examples.linuxapi_req
|
|
||||||
break
|
|
||||||
case 'xeapi':
|
|
||||||
this.isReq = false
|
|
||||||
this.encryptedData = ''
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
this.encryptedData = ''
|
|
||||||
}
|
|
||||||
if (this.encryptedData) this.decrypt()
|
|
||||||
},
|
|
||||||
loadExampleData(type) {
|
|
||||||
switch (type) {
|
|
||||||
case 'eapi_req':
|
|
||||||
this.crypto = 'eapi'
|
|
||||||
this.isReq = true
|
|
||||||
this.encryptedData = examples.eapi_req
|
|
||||||
break
|
|
||||||
case 'linuxapi_req':
|
|
||||||
this.crypto = 'linuxapi'
|
|
||||||
this.isReq = true
|
|
||||||
this.encryptedData = examples.linuxapi_req
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if (this.encryptedData) this.decrypt()
|
|
||||||
},
|
|
||||||
clearAll() {
|
|
||||||
this.encryptedData = ''
|
|
||||||
this.result = '{}'
|
|
||||||
},
|
|
||||||
copyResult() {
|
|
||||||
const text = typeof this.result === 'string' ? this.result : JSON.stringify(this.result, null, 2)
|
|
||||||
navigator.clipboard.writeText(text).then(() => {
|
|
||||||
alert('已复制到剪贴板')
|
|
||||||
})
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
app.mount('#app')
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@ -1,14 +1,13 @@
|
|||||||
# NeteaseCloudMusicAPI Enhanced
|
# 网易云音乐 API Enhanced
|
||||||
|
|
||||||
> 🎉 全网收集最全的网易云音乐api接口 基于[NeteaseCloudMusicAPI](https://github.com/binaryify/NeteaseCloudMusicApi)的复刻版本
|
> 🔍 网易云音乐API Node.js服务的复兴项目
|
||||||
|
|
||||||
- ⚡ 四种加密模式 · 后端代理
|
- 基于原版网易云API新增更多有趣的功能
|
||||||
- 🪛 具备多达200多个接口
|
- 具备登录接口,多达200多个接口
|
||||||
- 📄 更完善的文档
|
- 更完善的文档
|
||||||
|
|
||||||
|
|
||||||
[Github](https://github.com/neteasecloudmusicapienhanced/api-enhanced)
|
[Github](https://github.com/neteasecloudmusicapienhanced/api-enhanced)
|
||||||
[前往本家](https://github.com/binaryify/NeteaseCloudMusicApi)
|
[Get Started](#neteasecloudmusicapienhanced)
|
||||||
[快速开始](#neteasecloudmusicapienhanced)
|
|
||||||
|
|
||||||

|

|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.4 MiB |
@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
网易云音乐 NodeJS API Enhanced
|
网易云音乐 NodeJS API Enhanced
|
||||||
|
|
||||||
|
最后更新于: 2026.2.15
|
||||||
|
|
||||||
## 灵感来自
|
## 灵感来自
|
||||||
|
|
||||||
[disoul/electron-cloud-music](https://github.com/disoul/electron-cloud-music)
|
[disoul/electron-cloud-music](https://github.com/disoul/electron-cloud-music)
|
||||||
@ -88,7 +90,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 +103,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 +115,6 @@ export PORT=9000
|
|||||||
- 腾讯云 serverless 并不是免费的,前三个月有免费额度,之后收费
|
- 腾讯云 serverless 并不是免费的,前三个月有免费额度,之后收费
|
||||||
- 当前(2024-08-24), 用此法创建的话, 会`默认`关联一个"日志服务-日志主题"(创建过程中没有提醒), 此服务是计量收费的
|
- 当前(2024-08-24), 用此法创建的话, 会`默认`关联一个"日志服务-日志主题"(创建过程中没有提醒), 此服务是计量收费的
|
||||||
|
|
||||||
|
|
||||||
## 可以使用代理
|
## 可以使用代理
|
||||||
|
|
||||||
在 query 参数中加上 proxy=your-proxy 即可让这一次的请求使用 proxy
|
在 query 参数中加上 proxy=your-proxy 即可让这一次的请求使用 proxy
|
||||||
@ -183,7 +182,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
|
||||||
|
|
||||||
@ -213,20 +211,14 @@ $ sudo docker run -d -p 3000:3000 netease-music-api
|
|||||||
|
|
||||||
## 调试工具
|
## 调试工具
|
||||||
|
|
||||||
- 大部分请求参数或返回内容可在 `/api_decrypt.html` 里解析
|
- `eapi` 请求参数或返回内容可在 `/eapi_decrypt.html` 里解析
|
||||||
- 请求参数模式下, 解密结果可直接带到 `/api.html` 继续调试
|
- 请求参数模式下, 解密结果可直接带到 `/api.html` 继续调试
|
||||||
- 需要返回值加密时, 可传 `e_r=1`, `weapi` 和 `eapi` 都支持
|
- 需要返回值加密时, 可传 `e_r=1`, `weapi` 和 `eapi` 都支持
|
||||||
- 目前支持算法 有 `weapi`, `eapi`, `linuxapi` 和 `xeapi` (xeapi 是一种不加密的特殊算法, 主要用于调试加密前的原始请求参数)
|
|
||||||
|
|
||||||
|
|
||||||
## 接口文档
|
## 接口文档
|
||||||
|
|
||||||
### 调用前须知
|
### 调用前须知
|
||||||
|
|
||||||
AI 生成的图,仅供娱乐()
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
!> 本项目不提供线上 demo, 只提供在线文档服务, 请不要轻易信任使用他人提供的公开服务,以免发生安全问题,泄露自己的账号和密码
|
!> 本项目不提供线上 demo, 只提供在线文档服务, 请不要轻易信任使用他人提供的公开服务,以免发生安全问题,泄露自己的账号和密码
|
||||||
|
|
||||||
!> 为使用方便,降低门槛, 文档示例接口直接使用了 GET 请求,本项目同时支持 GET/POST 请按实际需求使用 (POST 请求 url 必须添加时间戳,使每次请求 url 不一样,不然请求会被缓存)
|
!> 为使用方便,降低门槛, 文档示例接口直接使用了 GET 请求,本项目同时支持 GET/POST 请按实际需求使用 (POST 请求 url 必须添加时间戳,使每次请求 url 不一样,不然请求会被缓存)
|
||||||
@ -269,7 +261,11 @@ AI 生成的图,仅供娱乐()
|
|||||||
|
|
||||||
!> ~~因网易增加了网易云盾验证,密码登录暂时不要使用,尽量使用短信验证码登录和二维码登录,否则调用某些接口会触发需要验证的错误~~
|
!> ~~因网易增加了网易云盾验证,密码登录暂时不要使用,尽量使用短信验证码登录和二维码登录,否则调用某些接口会触发需要验证的错误~~
|
||||||
|
|
||||||
!> 二开作者注: 在`v4.29.18`版本中修复了短信登录的问题, 现在可以正常使用了
|
!> ~~二开作者再注: 现在二维码登录也无法使用了, 网易云官方最近查的太严了, 现在尝试调用会提示环境异常, 如果各位有绕过的方法请一定开`Pull Request`~~
|
||||||
|
|
||||||
|
!> ~~二开作者注: 二维码登录现在是修复了, 但是密码登录和短信登录还是不行, 如果各位有绕过的方法请一定开`Pull Request`~~
|
||||||
|
|
||||||
|
!> 二开作者注: 在`v4.29.18`版本中修复了密码登录和短信登录的问题, 现在可以正常使用了
|
||||||
|
|
||||||
#### 1. 手机登录
|
#### 1. 手机登录
|
||||||
|
|
||||||
@ -1215,7 +1211,6 @@ tags: 歌单标签
|
|||||||
|
|
||||||
> 如果你设置 limit=50&offset=100,你就会得到第 101-150 首歌曲
|
> 如果你设置 limit=50&offset=100,你就会得到第 101-150 首歌曲
|
||||||
|
|
||||||
|
|
||||||
### 歌单详情动态
|
### 歌单详情动态
|
||||||
|
|
||||||
说明 : 调用后可获取歌单详情动态部分,如评论数,是否收藏,播放数
|
说明 : 调用后可获取歌单详情动态部分,如评论数,是否收藏,播放数
|
||||||
@ -1502,7 +1497,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 +1513,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 (...)
|
||||||
@ -1530,7 +1523,7 @@ tags: 歌单标签
|
|||||||
1. 歌词行显示开始时间戳 (毫秒)
|
1. 歌词行显示开始时间戳 (毫秒)
|
||||||
2. 歌词行显示总时长(毫秒)
|
2. 歌词行显示总时长(毫秒)
|
||||||
3. 逐字显示开始时间戳 (毫秒)
|
3. 逐字显示开始时间戳 (毫秒)
|
||||||
4. 逐字显示时长 (毫秒)
|
4. 逐字显示时长 (厘秒/0.01s)
|
||||||
5. 未知
|
5. 未知
|
||||||
6. 文字
|
6. 文字
|
||||||
|
|
||||||
@ -2500,30 +2493,6 @@ privilege:权限相关信息
|
|||||||
|
|
||||||
**调用例子 :** `/scrobble?id=518066366&sourceid=36780169&time=291`
|
**调用例子 :** `/scrobble?id=518066366&sourceid=36780169&time=291`
|
||||||
|
|
||||||
#### 听歌打卡 V2 (NCBL 加密版)
|
|
||||||
|
|
||||||
说明 : 调用此接口,使用桌面客户端 NCBL 加密日志格式上报听歌记录
|
|
||||||
|
|
||||||
**必选参数 :** `id`: 歌曲 id, `time`: 播放时长(秒)
|
|
||||||
|
|
||||||
**可选参数 :** `sourceid`: 来源列表 id, `source`: 来源名称(默认 list), `name`: 歌曲名, `artist`: 艺术家, `bitrate`: 码率(默认 320), `level`: 音质等级(默认 exhigh), `total`: 歌曲总时长(秒)
|
|
||||||
|
|
||||||
**接口地址 :** `/scrobble/v1`
|
|
||||||
|
|
||||||
**调用例子 :** `/scrobble/v1?id=518066366&sourceid=36780169&time=291`
|
|
||||||
|
|
||||||
### 提交歌曲播放状态
|
|
||||||
|
|
||||||
说明 : 调用此接口可提交歌曲播放状态,支持会话追踪和播放模式记录,未传入 `sessionId` 时后端会自动生成
|
|
||||||
|
|
||||||
**必选参数 :** `id`: 歌曲 id
|
|
||||||
|
|
||||||
**可选参数 :** `sessionId`: 播放会话 ID(12 位大写字母和数字),不传则自动生成, `progress`: 播放进度(秒),默认 0, `playMode`: 播放模式,默认 `list_loop`, `type`: 资源类型,默认 `song`
|
|
||||||
|
|
||||||
**接口地址 :** `/relay/play/state/submit`
|
|
||||||
|
|
||||||
**调用例子 :** `/relay/play/state/submit?id=518066366&progress=30`
|
|
||||||
|
|
||||||
### 热门歌手
|
### 热门歌手
|
||||||
|
|
||||||
说明 : 调用此接口 , 可获取热门歌手数据
|
说明 : 调用此接口 , 可获取热门歌手数据
|
||||||
@ -2905,7 +2874,6 @@ type : 地区
|
|||||||
- 适合 Vercel、Netlify 等有请求体限制的平台
|
- 适合 Vercel、Netlify 等有请求体限制的平台
|
||||||
- 需要前端配合实现
|
- 需要前端配合实现
|
||||||
|
|
||||||
|
|
||||||
#### 客户端直传相关接口
|
#### 客户端直传相关接口
|
||||||
|
|
||||||
**获取上传凭证**
|
**获取上传凭证**
|
||||||
@ -2952,7 +2920,6 @@ type : 地区
|
|||||||
- `artist`: 艺术家
|
- `artist`: 艺术家
|
||||||
- `album`: 专辑名
|
- `album`: 专辑名
|
||||||
|
|
||||||
|
|
||||||
#### 客户端直传流程
|
#### 客户端直传流程
|
||||||
|
|
||||||
1. 客户端计算文件 MD5
|
1. 客户端计算文件 MD5
|
||||||
@ -2960,7 +2927,6 @@ type : 地区
|
|||||||
3. 如果 `needUpload` 为 true,直接 PUT 文件到 `uploadUrl`
|
3. 如果 `needUpload` 为 true,直接 PUT 文件到 `uploadUrl`
|
||||||
4. 调用 `/cloud/upload/complete` 完成导入
|
4. 调用 `/cloud/upload/complete` 完成导入
|
||||||
|
|
||||||
|
|
||||||
### 云盘歌曲信息匹配纠正
|
### 云盘歌曲信息匹配纠正
|
||||||
|
|
||||||
说明 : 登录后调用此接口,可对云盘歌曲信息匹配纠正,如需取消匹配,asid 需要传 0
|
说明 : 登录后调用此接口,可对云盘歌曲信息匹配纠正,如需取消匹配,asid 需要传 0
|
||||||
@ -4233,7 +4199,6 @@ ONLINE 已发布
|
|||||||
- `voiceFeeType: 0`:返回免费的声音
|
- `voiceFeeType: 0`:返回免费的声音
|
||||||
- `voiceFeeType: 1`:返回收费的声音
|
- `voiceFeeType: 1`:返回收费的声音
|
||||||
|
|
||||||
|
|
||||||
### 播客声音详情
|
### 播客声音详情
|
||||||
|
|
||||||
说明: 获取播客里的声音详情
|
说明: 获取播客里的声音详情
|
||||||
@ -5066,7 +5031,7 @@ let data = encodeURIComponent(
|
|||||||
|
|
||||||
**调用例子:** `/vip/sign`
|
**调用例子:** `/vip/sign`
|
||||||
|
|
||||||
### 黑胶乐签未来打卡信息
|
### 黑胶乐签打卡信息
|
||||||
|
|
||||||
说明: 登录后调用此接口, 获取黑胶乐签打卡信息
|
说明: 登录后调用此接口, 获取黑胶乐签打卡信息
|
||||||
|
|
||||||
@ -5374,56 +5339,6 @@ let data = encodeURIComponent(
|
|||||||
|
|
||||||
说明 : 调用此接口,可获取城市榜、城市风格榜等指定维度音乐排行榜歌曲列表
|
说明 : 调用此接口,可获取城市榜、城市风格榜等指定维度音乐排行榜歌曲列表
|
||||||
|
|
||||||
**必选参数 :**
|
|
||||||
|
|
||||||
`chartCode`: 榜单编码,如 `CITY_SONG_CHART`、`CITY_STYLE_SONG_CHART`
|
|
||||||
|
|
||||||
`targetId`: 目标 id,城市榜如 `110000`,城市风格榜如 北京华语流行榜 `110000_1020`。城市风格榜格式通常为 `城市 id_曲风 id`,其中曲风 id 可通过[曲风列表](#曲风列表)接口 `/style/list` 获取。城市榜的城市列表可通过[多级行政区划数据](#多级行政区划数据)接口传入 `bizCode=chart` 获取;城市风格榜的城市列表可通过该接口传空 `bizCode` 获取
|
|
||||||
|
|
||||||
`targetType`: 目标类型,如 `CITY`、`CITY_STYLE`
|
|
||||||
|
|
||||||
**接口地址 :** `/chart/song/detail`
|
|
||||||
|
|
||||||
**调用例子 :** `/chart/song/detail?chartCode=CITY_STYLE_SONG_CHART&targetId=110000_1020&targetType=CITY_STYLE`
|
|
||||||
|
|
||||||
### 会员任务 - 新版
|
|
||||||
|
|
||||||
说明 : 登录后调用此接口, 获取会员任务
|
|
||||||
|
|
||||||
**可选参数** `id`: 用户 id, 传入后可获取指定用户的会员任务, 不传入则获取当前登录用户的会员任务
|
|
||||||
|
|
||||||
**接口地址 :** `/vip/task/v1`
|
|
||||||
|
|
||||||
**调用例子 :** `/vip/task/v1` `/vip/task/v1?id=32953014`
|
|
||||||
|
|
||||||
### 黑胶乐签详情
|
|
||||||
|
|
||||||
说明 : 登录后调用此接口, 传入时间戳, 获取黑胶乐签详情
|
|
||||||
|
|
||||||
**必选参数 :** `timestamp`: 时间戳, 单位毫秒, 如 `1704067200000` 表示 2024 年 12 月 31 日 0 点 (不传入会出现随机的乐签详情)
|
|
||||||
|
|
||||||
**接口地址 :** `/vip/sign/detail`
|
|
||||||
|
|
||||||
**调用例子 :** `/vip/sign/detail`
|
|
||||||
|
|
||||||
### 黑胶乐签历史
|
|
||||||
|
|
||||||
说明 : 登录后调用此接口, 获取黑胶乐签历史
|
|
||||||
|
|
||||||
**接口地址 :** `/vip/sign/history`
|
|
||||||
|
|
||||||
**调用例子 :** `/vip/sign/history`
|
|
||||||
|
|
||||||
### 直接获取云盘歌曲下载链接
|
|
||||||
|
|
||||||
说明 : 调用此接口, 传入云盘歌曲 id, 可直接获取云盘歌曲下载链接
|
|
||||||
|
|
||||||
**必选参数 :** `id`: 云盘歌曲 id
|
|
||||||
|
|
||||||
**接口地址 :** `/song/cloud/download`
|
|
||||||
|
|
||||||
**调用例子 :** `/song/cloud/download?id=123456789`
|
|
||||||
|
|
||||||
## 离线访问此文档
|
## 离线访问此文档
|
||||||
|
|
||||||
此文档同时也是 Progressive Web Apps(PWA), 加入了 serviceWorker, 可离线访问
|
此文档同时也是 Progressive Web Apps(PWA), 加入了 serviceWorker, 可离线访问
|
||||||
|
|||||||
BIN
public/docs/icon.png
Normal file
BIN
public/docs/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 858 KiB |
@ -108,7 +108,7 @@
|
|||||||
name: '网易云音乐 API Enhanced',
|
name: '网易云音乐 API Enhanced',
|
||||||
repo: 'https://github.com/neteasecloudmusicapienhanced/api-enhanced',
|
repo: 'https://github.com/neteasecloudmusicapienhanced/api-enhanced',
|
||||||
coverpage: true,
|
coverpage: true,
|
||||||
homepage: 'home.md',
|
homepage: 'https://cdn.jsdelivr.net/gh/NeteaseCloudMusicApiEnhanced/api-enhanced@main/public/docs/home.md',
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<script src="https://unpkg.com/docsify@4.11.3/lib/docsify.min.js"></script>
|
<script src="https://unpkg.com/docsify@4.11.3/lib/docsify.min.js"></script>
|
||||||
|
|||||||
265
public/eapi_decrypt.html
Normal file
265
public/eapi_decrypt.html
Normal file
@ -0,0 +1,265 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>eapi 参数和返回内容解析</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f5f5f5;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 32px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #555;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 200px;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea:focus {
|
||||||
|
border-color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 24px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-item input[type="radio"] {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-item label {
|
||||||
|
margin: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background: #333;
|
||||||
|
color: white;
|
||||||
|
padding: 12px 28px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
button + button {
|
||||||
|
margin-left: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-section {
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-section label {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.decode-result {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
background: #f9f9f9;
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
min-height: 200px;
|
||||||
|
max-height: 400px;
|
||||||
|
overflow: auto;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-section {
|
||||||
|
margin-top: 32px;
|
||||||
|
padding-top: 24px;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-section h2 {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-section img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="app" class="container">
|
||||||
|
<h1>eapi 参数和返回内容解析</h1>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="hexString">十六进制字符串</label>
|
||||||
|
<textarea id="hexString" v-model="hexString" rows="10"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="radio-group">
|
||||||
|
<div class="radio-item">
|
||||||
|
<input type="radio" id="req" name="format" v-model="isReq" value="true">
|
||||||
|
<label for="req">请求数据 request params</label>
|
||||||
|
</div>
|
||||||
|
<div class="radio-item">
|
||||||
|
<input type="radio" id="resp" name="format" v-model="isReq" value="false">
|
||||||
|
<label for="resp">返回数据 response 二进制数据</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button @click="decrypt">解密</button>
|
||||||
|
<button @click="sendToApi" :disabled="!canSend" :class="[{ 'opacity-50 cursor-not-allowed pointer-events-none': !canSend },]">填入 API 调试</button>
|
||||||
|
|
||||||
|
<div class="result-section">
|
||||||
|
<label>解密结果:</label>
|
||||||
|
<pre class="decode-result">{{ formatResult(result) }}</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="example-section">
|
||||||
|
<h2>使用示例</h2>
|
||||||
|
<img src="/static/eapi_params.png" alt="请求示例" />
|
||||||
|
<img src="/static/eapi_response.png" alt="响应示例" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://fastly.jsdelivr.net/npm/axios"></script>
|
||||||
|
<script src="https://fastly.jsdelivr.net/npm/vue@3"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const app = Vue.createApp({
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
hexString: 'AD96DDB984491E79B6F429DD650C6E2AE524627AC223AC9A123C66BB0997965950FED137544A93DFC718E16F57C8C121AF537086F395570A5602A3922366D11964DAFACD7830AACABF62E5650E67F457E79C1D2E13502391FC3487216CC5BF8681843FCB8E05559487EB18AAC1BE0EFEA4F7B6A050478366153A9426C238B8869600B275704555A9EB94C92E4F3FDABE9E0BCE07645410D0AA7B675698A4CAE6CD3620633ABF0B849A4244CC8DFC5DB2646D5EA9B3954E62BFEF19AFEAFDDC34E55C3E9A1DD3167CF53D443617108141',
|
||||||
|
result: '{}',
|
||||||
|
isReq: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.decrypt()
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
isRequestMode() {
|
||||||
|
return this.isReq === true || this.isReq === 'true'
|
||||||
|
},
|
||||||
|
canSend() {
|
||||||
|
if (!this.isRequestMode) return false
|
||||||
|
if (!this.result || this.result === '{}' || this.result === 'null') return false
|
||||||
|
try {
|
||||||
|
JSON.parse(this.result)
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
formatResult(value) {
|
||||||
|
if (value == null || value === '') return ''
|
||||||
|
try {
|
||||||
|
const parsed = typeof value === 'string' ? JSON.parse(value) : value
|
||||||
|
return JSON.stringify(parsed, null, 2)
|
||||||
|
} catch (error) {
|
||||||
|
return String(value)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async decrypt() {
|
||||||
|
try {
|
||||||
|
const res = await axios({
|
||||||
|
url: `/eapi/decrypt?isReq=${this.isReq}×tamp=${Date.now()}`,
|
||||||
|
method: 'post',
|
||||||
|
data: {
|
||||||
|
hexString: this.hexString
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.result = JSON.stringify(res.data.data)
|
||||||
|
console.log(res.data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
alert(error?.response?.data?.message || '解密失败,数据格式错误')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
sendToApi() {
|
||||||
|
if (!this.canSend) return
|
||||||
|
const payload = JSON.parse(this.result)
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
params.set('uri', payload.uri || payload.url || payload.path || '')
|
||||||
|
params.set('crypto', 'eapi')
|
||||||
|
const data =
|
||||||
|
payload.params ||
|
||||||
|
payload.data ||
|
||||||
|
payload.body ||
|
||||||
|
payload.payload ||
|
||||||
|
payload.request ||
|
||||||
|
{}
|
||||||
|
params.set('data', JSON.stringify(data))
|
||||||
|
window.open(`/api.html?${params.toString()}`, '_blank')
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
app.mount('#app')
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@ -92,11 +92,10 @@ curl -s {origin}/search?keywords=网易云</code></pre>
|
|||||||
<a href="/audio_match_demo/index.html">听歌识曲 Demo</a> ·
|
<a href="/audio_match_demo/index.html">听歌识曲 Demo</a> ·
|
||||||
<a href="/cloud.html">云盘上传</a> ·
|
<a href="/cloud.html">云盘上传</a> ·
|
||||||
<a href="/playlist_import.html">歌单导入</a> ·
|
<a href="/playlist_import.html">歌单导入</a> ·
|
||||||
<a href="/api_decrypt.html">API 解密</a> ·
|
<a href="/eapi_decrypt.html">EAPI 解密</a> ·
|
||||||
<a href="/listen_together_host.html">一起听示例</a> ·
|
<a href="/listen_together_host.html">一起听示例</a> ·
|
||||||
<a href="/playlist_cover_update.html">更新歌单封面示例</a> ·
|
<a href="/playlist_cover_update.html">更新歌单封面示例</a> ·
|
||||||
<a href="/avatar_update.html">头像更新示例</a> ·
|
<a href="/avatar_update.html">头像更新示例</a>
|
||||||
<a href="/scrobble.html">听歌打卡示例</a>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,480 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="zh">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>听歌打卡 - 网易云音乐 API Enhanced</title>
|
|
||||||
<style>
|
|
||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
||||||
min-height: 100vh;
|
|
||||||
background: #f5f5f5;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
max-width: 520px;
|
|
||||||
margin: 40px auto;
|
|
||||||
background: white;
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 32px;
|
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 24px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #333;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subtitle {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #666;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-link {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
color: #666;
|
|
||||||
font-size: 14px;
|
|
||||||
text-decoration: none;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-link:hover {
|
|
||||||
color: #333;
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---- Tabs ---- */
|
|
||||||
.tabs {
|
|
||||||
display: flex;
|
|
||||||
gap: 0;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
border-bottom: 2px solid #eee;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-btn {
|
|
||||||
flex: 1;
|
|
||||||
padding: 10px 0;
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
border-bottom: 2px solid transparent;
|
|
||||||
margin-bottom: -2px;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #999;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: color 0.2s, border-color 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-btn:hover {
|
|
||||||
color: #555;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-btn.active {
|
|
||||||
color: #333;
|
|
||||||
border-bottom-color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-content {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-content.active {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---- Form ---- */
|
|
||||||
.form-group {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
|
||||||
display: block;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #555;
|
|
||||||
margin-bottom: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
label .tag {
|
|
||||||
display: inline-block;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 400;
|
|
||||||
padding: 1px 6px;
|
|
||||||
border-radius: 3px;
|
|
||||||
margin-left: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
label .tag.r {
|
|
||||||
background: #fee2e2;
|
|
||||||
color: #991b1b;
|
|
||||||
}
|
|
||||||
|
|
||||||
label .tag.o {
|
|
||||||
background: #fef3c7;
|
|
||||||
color: #92400e;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="text"],
|
|
||||||
input[type="number"],
|
|
||||||
textarea {
|
|
||||||
width: 100%;
|
|
||||||
padding: 10px 14px;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 14px;
|
|
||||||
outline: none;
|
|
||||||
font-family: inherit;
|
|
||||||
transition: border-color 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="text"]:focus,
|
|
||||||
input[type="number"]:focus,
|
|
||||||
textarea:focus {
|
|
||||||
border-color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
textarea {
|
|
||||||
resize: vertical;
|
|
||||||
}
|
|
||||||
|
|
||||||
.row {
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.row .form-group {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quick-fill {
|
|
||||||
margin-top: 5px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quick-fill span {
|
|
||||||
color: #0066cc;
|
|
||||||
cursor: pointer;
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quick-fill span:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
padding: 12px;
|
|
||||||
background: #333;
|
|
||||||
color: white;
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 500;
|
|
||||||
border-radius: 6px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background 0.2s ease;
|
|
||||||
border: none;
|
|
||||||
text-align: center;
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn:hover {
|
|
||||||
background: #555;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn:disabled {
|
|
||||||
background: #ccc;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result {
|
|
||||||
margin-top: 16px;
|
|
||||||
padding: 12px 16px;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 14px;
|
|
||||||
text-align: left;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
word-break: break-all;
|
|
||||||
font-family: monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result.success {
|
|
||||||
background: #d1fae5;
|
|
||||||
color: #065f46;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result.error {
|
|
||||||
background: #fee2e2;
|
|
||||||
color: #991b1b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result.info {
|
|
||||||
background: #e0f2fe;
|
|
||||||
color: #0369a1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-link {
|
|
||||||
text-align: center;
|
|
||||||
margin-top: 24px;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-link a {
|
|
||||||
color: #666;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-link a:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<h1>听歌打卡</h1>
|
|
||||||
<p class="subtitle">同步听歌记录至网易云音乐,增加听歌排行计数</p>
|
|
||||||
|
|
||||||
<a href="/qrlogin-nocookie.html" class="login-link">还没登录?点击登录</a>
|
|
||||||
|
|
||||||
<!-- Cookie (共享) -->
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="cookie">Cookie <span class="tag o">可选</span></label>
|
|
||||||
<textarea id="cookie" placeholder="留空则默认读取本地存储的登录态" rows="2"></textarea>
|
|
||||||
<div class="quick-fill">
|
|
||||||
<span onclick="loadLocalCookie()">读取本地 Cookie</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Tabs -->
|
|
||||||
<div class="tabs">
|
|
||||||
<button class="tab-btn active" data-tab="v0" onclick="switchTab('v0')">原版</button>
|
|
||||||
<button class="tab-btn" data-tab="v1" onclick="switchTab('v1')">V2 (NCBL 加密版)</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ============ Tab: v0 (原版 /scrobble) ============ -->
|
|
||||||
<div class="tab-content active" id="tab-v0">
|
|
||||||
<div class="form-group">
|
|
||||||
<label>歌曲 ID <span class="tag r">必填</span></label>
|
|
||||||
<input type="text" id="v0-songId" placeholder="例如: 2756058128" value="2756058128" />
|
|
||||||
<div class="quick-fill">
|
|
||||||
示例:
|
|
||||||
<span onclick="fillV0('2756058128', '288651229')">妖精小姐的魔法邀约</span>
|
|
||||||
<span onclick="fillV0('2637402867', '251025018')">Echoes of Memoria</span>
|
|
||||||
<span onclick="fillV0('36307815', '3394198')">Lose Control</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>来源 ID (歌单或专辑 ID) <span class="tag r">必填</span></label>
|
|
||||||
<input type="text" id="v0-sourceId" placeholder="例如: 2756058128" value="2756058128" />
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>播放时间 (秒) <span class="tag o">可选</span></label>
|
|
||||||
<input type="number" id="v0-time" placeholder="300" value="300" />
|
|
||||||
</div>
|
|
||||||
<button class="btn" onclick="submitV0()">立即打卡 (原版)</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ============ Tab: v1 (NCBL 加密版 /scrobble/v1) ============ -->
|
|
||||||
<div class="tab-content" id="tab-v1">
|
|
||||||
<div class="form-group">
|
|
||||||
<label>歌曲 ID <span class="tag r">必填</span></label>
|
|
||||||
<input type="text" id="v1-songId" placeholder="例如: 518066366" value="518066366" />
|
|
||||||
<div class="quick-fill">
|
|
||||||
示例:
|
|
||||||
<span onclick="fillV1('518066366', '36780169', '291')">默认示例</span>
|
|
||||||
<span onclick="fillV1('2756058128', '288651229', '300')">妖精小姐的魔法邀约</span>
|
|
||||||
<span onclick="fillV1('2637402867', '251025018', '300')">Echoes of Memoria</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>播放时间 (秒) <span class="tag r">必填</span></label>
|
|
||||||
<input type="number" id="v1-time" placeholder="例如: 291" value="291" />
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>来源 ID <span class="tag o">可选</span></label>
|
|
||||||
<input type="text" id="v1-sourceId" placeholder="歌单或专辑 ID" value="36780169" />
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="form-group">
|
|
||||||
<label>歌曲名 <span class="tag o">可选</span></label>
|
|
||||||
<input type="text" id="v1-name" placeholder="歌曲名称" />
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>艺术家 <span class="tag o">可选</span></label>
|
|
||||||
<input type="text" id="v1-artist" placeholder="歌手名" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="form-group">
|
|
||||||
<label>码率 <span class="tag o">可选</span></label>
|
|
||||||
<input type="number" id="v1-bitrate" placeholder="默认 320" value="320" />
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>音质等级 <span class="tag o">可选</span></label>
|
|
||||||
<input type="text" id="v1-level" placeholder="默认 exhigh" value="exhigh" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>歌曲总时长 (秒) <span class="tag o">可选</span></label>
|
|
||||||
<input type="number" id="v1-total" placeholder="留空则与播放时间相同" />
|
|
||||||
</div>
|
|
||||||
<button class="btn" onclick="submitV1()">立即打卡 (NCBL 加密版)</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Result -->
|
|
||||||
<div id="result" class="result" style="display: none;"></div>
|
|
||||||
|
|
||||||
<div class="footer-link">
|
|
||||||
<a href="/">返回首页</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="https://fastly.jsdelivr.net/npm/axios@0.26.1/dist/axios.min.js"></script>
|
|
||||||
<script>
|
|
||||||
/* ---- DOM refs ---- */
|
|
||||||
const cookieInput = document.getElementById('cookie')
|
|
||||||
const resultDiv = document.getElementById('result')
|
|
||||||
|
|
||||||
/* ---- init ---- */
|
|
||||||
if (localStorage.getItem('cookie')) {
|
|
||||||
cookieInput.value = localStorage.getItem('cookie')
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---- Tab switching ---- */
|
|
||||||
function switchTab(name) {
|
|
||||||
document.querySelectorAll('.tab-btn').forEach(b => b.classList.toggle('active', b.dataset.tab === name))
|
|
||||||
document.querySelectorAll('.tab-content').forEach(c => c.classList.toggle('active', c.id === 'tab-' + name))
|
|
||||||
hideResult()
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---- Cookie ---- */
|
|
||||||
function loadLocalCookie() {
|
|
||||||
const local = localStorage.getItem('cookie')
|
|
||||||
if (local) {
|
|
||||||
cookieInput.value = local
|
|
||||||
showResult('已成功读取本地 Cookie', 'success')
|
|
||||||
} else {
|
|
||||||
showResult('未在本地发现登录 Cookie,请先登录或手动粘贴', 'error')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---- Quick fill ---- */
|
|
||||||
function fillV0(id, sourceId) {
|
|
||||||
document.getElementById('v0-songId').value = id
|
|
||||||
document.getElementById('v0-sourceId').value = sourceId
|
|
||||||
}
|
|
||||||
|
|
||||||
function fillV1(id, sourceId, time) {
|
|
||||||
document.getElementById('v1-songId').value = id
|
|
||||||
document.getElementById('v1-sourceId').value = sourceId
|
|
||||||
document.getElementById('v1-time').value = time
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---- Result ---- */
|
|
||||||
function hideResult() {
|
|
||||||
resultDiv.style.display = 'none'
|
|
||||||
}
|
|
||||||
|
|
||||||
function showResult(message, type) {
|
|
||||||
resultDiv.textContent = message
|
|
||||||
resultDiv.className = 'result ' + type
|
|
||||||
resultDiv.style.display = 'block'
|
|
||||||
}
|
|
||||||
|
|
||||||
function disableAll(disabled) {
|
|
||||||
document.querySelectorAll('.btn').forEach(b => b.disabled = disabled)
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---- Submit: 原版 /scrobble ---- */
|
|
||||||
async function submitV0() {
|
|
||||||
const id = document.getElementById('v0-songId').value.trim()
|
|
||||||
const sourceid = document.getElementById('v0-sourceId').value.trim()
|
|
||||||
const time = document.getElementById('v0-time').value.trim() || '300'
|
|
||||||
|
|
||||||
if (!id) { showResult('请输入歌曲 ID !', 'error'); return }
|
|
||||||
if (!sourceid) { showResult('请输入来源 ID !', 'error'); return }
|
|
||||||
|
|
||||||
disableAll(true)
|
|
||||||
showResult('打卡中,请稍候...', 'info')
|
|
||||||
|
|
||||||
try {
|
|
||||||
const params = { id, sourceid: sourceid || id, time, timestamp: Date.now() }
|
|
||||||
const cookie = cookieInput.value.trim()
|
|
||||||
if (cookie) params.cookie = cookie
|
|
||||||
|
|
||||||
const res = await axios({ method: 'get', url: '/scrobble', params })
|
|
||||||
if (res.data.code === 200) {
|
|
||||||
showResult('✅ 打卡成功! \n\n' + JSON.stringify(res.data, null, 2), 'success')
|
|
||||||
} else {
|
|
||||||
showResult('请求已发送,返回:\n\n' + JSON.stringify(res.data, null, 2), 'info')
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
const errData = error.response ? error.response.data : { error: error.message }
|
|
||||||
showResult('❌ 打卡失败\n\n' + JSON.stringify(errData, null, 2), 'error')
|
|
||||||
} finally {
|
|
||||||
disableAll(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---- Submit: NCBL 加密版 /scrobble/v1 ---- */
|
|
||||||
async function submitV1() {
|
|
||||||
const id = document.getElementById('v1-songId').value.trim()
|
|
||||||
const time = document.getElementById('v1-time').value.trim()
|
|
||||||
|
|
||||||
if (!id) { showResult('请输入歌曲 ID !', 'error'); return }
|
|
||||||
if (!time || isNaN(+time) || +time <= 0) { showResult('请输入有效的播放时长(秒)!', 'error'); return }
|
|
||||||
|
|
||||||
disableAll(true)
|
|
||||||
showResult('打卡中(NCBL 加密),请稍候...', 'info')
|
|
||||||
|
|
||||||
try {
|
|
||||||
const params = {
|
|
||||||
id,
|
|
||||||
time,
|
|
||||||
sourceid: document.getElementById('v1-sourceId').value.trim() || undefined,
|
|
||||||
name: document.getElementById('v1-name').value.trim() || undefined,
|
|
||||||
artist: document.getElementById('v1-artist').value.trim() || undefined,
|
|
||||||
bitrate: document.getElementById('v1-bitrate').value.trim() || undefined,
|
|
||||||
level: document.getElementById('v1-level').value.trim() || undefined,
|
|
||||||
total: document.getElementById('v1-total').value.trim() || undefined,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
}
|
|
||||||
const cookie = cookieInput.value.trim()
|
|
||||||
if (cookie) params.cookie = cookie
|
|
||||||
|
|
||||||
// 清理 undefined 参数
|
|
||||||
Object.keys(params).forEach(k => params[k] === undefined && delete params[k])
|
|
||||||
|
|
||||||
const res = await axios({ method: 'get', url: '/scrobble/v1', params })
|
|
||||||
if (res.data.code === 200) {
|
|
||||||
showResult('✅ NCBL 加密打卡成功! \n\n' + JSON.stringify(res.data, null, 2), 'success')
|
|
||||||
} else {
|
|
||||||
showResult('请求已发送,返回:\n\n' + JSON.stringify(res.data, null, 2), 'info')
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
const errData = error.response ? error.response.data : { error: error.message }
|
|
||||||
showResult('❌ 打卡失败\n\n' + JSON.stringify(errData, null, 2), 'error')
|
|
||||||
} finally {
|
|
||||||
disableAll(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
BIN
public/static/2169.png
Normal file
BIN
public/static/2169.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 992 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.4 MiB |
@ -1,577 +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)
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
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 }
|
|
||||||
21
server.js
21
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 (
|
||||||
@ -425,7 +422,7 @@ async function serveNcmApi(options) {
|
|||||||
options.checkVersion &&
|
options.checkVersion &&
|
||||||
checkVersion().then(({ npmVersion, ourVersion, status }) => {
|
checkVersion().then(({ npmVersion, ourVersion, status }) => {
|
||||||
if (status == VERSION_CHECK_RESULT.NOT_LATEST) {
|
if (status == VERSION_CHECK_RESULT.NOT_LATEST) {
|
||||||
logger.warn(
|
logger.info(
|
||||||
`最新版本: ${npmVersion}, 当前版本: ${ourVersion}, 请及时更新`,
|
`最新版本: ${npmVersion}, 当前版本: ${ourVersion}, 请及时更新`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -447,9 +444,11 @@ 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
|
||||||
|
|||||||
@ -11,10 +11,7 @@
|
|||||||
},
|
},
|
||||||
"APP_CONF": {
|
"APP_CONF": {
|
||||||
"apiDomain": "https://interface.music.163.com",
|
"apiDomain": "https://interface.music.163.com",
|
||||||
"xeapiDomain": "https://interface3.music.163.com",
|
|
||||||
"domain": "https://music.163.com",
|
"domain": "https://music.163.com",
|
||||||
"clDomian": "https://clientlog.music.163.com",
|
|
||||||
"clDomian3": "https://clientlog3.music.163.com",
|
|
||||||
"encrypt": true,
|
"encrypt": true,
|
||||||
"encryptResponse": false,
|
"encryptResponse": false,
|
||||||
"clientSign": "18:C0:4D:B9:8F:FE@@@453832335F384641365F424635335F303030315F303031425F343434415F343643365F333638332@@@@@@6ff673ef74955b38bce2fa8562d95c976ed4758b1227c4e9ee345987cee17bc9",
|
"clientSign": "18:C0:4D:B9:8F:FE@@@453832335F384641365F424635335F303030315F303031425F343434415F343643365F333638332@@@@@@6ff673ef74955b38bce2fa8562d95c976ed4758b1227c4e9ee345987cee17bc9",
|
||||||
|
|||||||
167
util/crypto.js
167
util/crypto.js
@ -1,5 +1,4 @@
|
|||||||
const CryptoJS = require('crypto-js')
|
const CryptoJS = require('crypto-js')
|
||||||
const crypto = require('crypto')
|
|
||||||
const forge = require('node-forge')
|
const forge = require('node-forge')
|
||||||
const zlib = require('zlib')
|
const zlib = require('zlib')
|
||||||
const iv = '0102030405060708'
|
const iv = '0102030405060708'
|
||||||
@ -10,13 +9,6 @@ const publicKey = `-----BEGIN PUBLIC KEY-----
|
|||||||
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDgtQn2JZ34ZC28NWYpAUd98iZ37BUrX/aKzmFbt7clFSs6sXqHauqKWqdtLkF2KexO40H1YTX8z2lSgBBOAxLsvaklV8k4cBFK9snQXE9/DDaFt6Rr7iVZMldczhC0JNgTz+SHXT6CBHuX3e9SdB1Ua44oncaTWz7OBGLbCiK45wIDAQAB
|
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDgtQn2JZ34ZC28NWYpAUd98iZ37BUrX/aKzmFbt7clFSs6sXqHauqKWqdtLkF2KexO40H1YTX8z2lSgBBOAxLsvaklV8k4cBFK9snQXE9/DDaFt6Rr7iVZMldczhC0JNgTz+SHXT6CBHuX3e9SdB1Ua44oncaTWz7OBGLbCiK45wIDAQAB
|
||||||
-----END PUBLIC KEY-----`
|
-----END PUBLIC KEY-----`
|
||||||
const eapiKey = 'e82ckenh8dichen8'
|
const eapiKey = 'e82ckenh8dichen8'
|
||||||
const xeapiStaticKey = Buffer.from(
|
|
||||||
'ab1d5a430f6bb04a3f01e81ddd72bd916d5ce591248ac128714806d7f8fb1b84',
|
|
||||||
'hex',
|
|
||||||
)
|
|
||||||
const xeapiSignKey =
|
|
||||||
'mUHCwVNWJbunMqAHf5MImuirT6plvs6VSFW62MGHstFQxhBGdEoIhLItH3djc4+FB/OKty3+lL2rGeoFBpVe5g=='
|
|
||||||
const x25519SpkiPrefix = Buffer.from('302a300506032b656e032100', 'hex')
|
|
||||||
|
|
||||||
const aesEncrypt = (text, mode, key, iv, format = 'base64') => {
|
const aesEncrypt = (text, mode, key, iv, format = 'base64') => {
|
||||||
let encrypted = CryptoJS.AES.encrypt(
|
let encrypted = CryptoJS.AES.encrypt(
|
||||||
@ -149,172 +141,13 @@ const decrypt = (cipher) => {
|
|||||||
return decryptedBytes
|
return decryptedBytes
|
||||||
}
|
}
|
||||||
|
|
||||||
const aesEcbEncrypt = (key, plaintext) => {
|
|
||||||
const cipher = crypto.createCipheriv(`aes-${key.length * 8}-ecb`, key, null)
|
|
||||||
return Buffer.concat([cipher.update(Buffer.from(plaintext)), cipher.final()])
|
|
||||||
}
|
|
||||||
|
|
||||||
const aesEcbDecrypt = (key, ciphertext) => {
|
|
||||||
const decipher = crypto.createDecipheriv(
|
|
||||||
`aes-${key.length * 8}-ecb`,
|
|
||||||
key,
|
|
||||||
null,
|
|
||||||
)
|
|
||||||
return Buffer.concat([decipher.update(ciphertext), decipher.final()])
|
|
||||||
}
|
|
||||||
|
|
||||||
const createX25519PublicKey = (raw) => {
|
|
||||||
// Node's crypto API expects X25519 public keys as DER SubjectPublicKeyInfo.
|
|
||||||
// The Android SDK stores only the 32-byte raw key, so prepend the fixed
|
|
||||||
// RFC 8410 SPKI header for id-X25519 before importing it.
|
|
||||||
return crypto.createPublicKey({
|
|
||||||
key: Buffer.concat([x25519SpkiPrefix, raw]),
|
|
||||||
format: 'der',
|
|
||||||
type: 'spki',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const deriveX25519AesKey = (sharedSecret, ephemeralPublicKey) => {
|
|
||||||
const prk = crypto
|
|
||||||
.createHmac('sha256', Buffer.alloc(32))
|
|
||||||
.update(sharedSecret.length ? sharedSecret : Buffer.alloc(32))
|
|
||||||
.digest()
|
|
||||||
return crypto
|
|
||||||
.createHmac('sha256', prk)
|
|
||||||
.update(Buffer.concat([ephemeralPublicKey, Buffer.from([1])]))
|
|
||||||
.digest()
|
|
||||||
.subarray(0, 16)
|
|
||||||
}
|
|
||||||
|
|
||||||
const xeapiSign = (timestamp, nonce) => {
|
|
||||||
return crypto
|
|
||||||
.createHmac('sha256', xeapiSignKey)
|
|
||||||
.update(String(timestamp) + nonce)
|
|
||||||
.digest('base64')
|
|
||||||
}
|
|
||||||
|
|
||||||
const xeapiMidTransform = (ciphertext) => {
|
|
||||||
const random = crypto.randomBytes(16)
|
|
||||||
const xored = Buffer.alloc(ciphertext.length)
|
|
||||||
for (let i = 0; i < ciphertext.length; i++) {
|
|
||||||
xored[i] = ciphertext[i] ^ random[i & 0x0f]
|
|
||||||
}
|
|
||||||
const b64 = Buffer.from(xored.toString('base64'))
|
|
||||||
const rot = b64.length ? (random[0] & 0x0f) % b64.length : 0
|
|
||||||
return Buffer.concat([random, b64.subarray(rot), b64.subarray(0, rot)])
|
|
||||||
}
|
|
||||||
|
|
||||||
const xeapiEncryptS = (dynamicKey, publicKeyState, os) => {
|
|
||||||
const peerRaw = Buffer.from(publicKeyState.publicKey, 'base64')
|
|
||||||
const peerKey = createX25519PublicKey(peerRaw)
|
|
||||||
const { publicKey, privateKey } = crypto.generateKeyPairSync('x25519')
|
|
||||||
const ephemeralRaw = Buffer.from(
|
|
||||||
publicKey.export({ format: 'der', type: 'spki' }),
|
|
||||||
).subarray(-32)
|
|
||||||
const sharedSecret = crypto.diffieHellman({
|
|
||||||
privateKey,
|
|
||||||
publicKey: peerKey,
|
|
||||||
})
|
|
||||||
const aesKey = deriveX25519AesKey(sharedSecret, ephemeralRaw)
|
|
||||||
const iv = crypto.randomBytes(12)
|
|
||||||
const cipher = crypto.createCipheriv('aes-128-gcm', aesKey, iv)
|
|
||||||
const plaintext = Buffer.from(
|
|
||||||
`${dynamicKey.toString('base64')}|${os}|${publicKeyState.sk || ''}`,
|
|
||||||
)
|
|
||||||
const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()])
|
|
||||||
return Buffer.concat([ephemeralRaw, iv, encrypted, cipher.getAuthTag()])
|
|
||||||
}
|
|
||||||
|
|
||||||
const buildXeapiPlaintext = (uri, data, options = {}) => {
|
|
||||||
const fields = {}
|
|
||||||
const contentType =
|
|
||||||
options.contentType || 'application/x-www-form-urlencoded;charset=utf-8'
|
|
||||||
const mediaType = contentType.split(';', 1)[0].toLowerCase()
|
|
||||||
if (mediaType !== 'application/x-www-form-urlencoded') {
|
|
||||||
fields.contentType = contentType
|
|
||||||
}
|
|
||||||
|
|
||||||
const method = (options.method || 'POST').toUpperCase()
|
|
||||||
if (method !== 'POST') fields.method = method
|
|
||||||
|
|
||||||
const url = new URL(uri, 'https://interface.music.163.com')
|
|
||||||
if (url.search) fields.queryString = url.search.slice(1)
|
|
||||||
|
|
||||||
if (data !== undefined && data !== null) {
|
|
||||||
const bodyData = { ...data }
|
|
||||||
delete bodyData.e_r
|
|
||||||
const body = Buffer.from(new URLSearchParams(bodyData).toString())
|
|
||||||
fields.body = body.toString('base64')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fields.queryString) {
|
|
||||||
fields.queryString += '&e_r=true'
|
|
||||||
} else {
|
|
||||||
fields.queryString = 'e_r=true'
|
|
||||||
}
|
|
||||||
return JSON.stringify(fields)
|
|
||||||
}
|
|
||||||
|
|
||||||
const xeapi = (uri, data, options = {}) => {
|
|
||||||
const publicKeyState = options.publicKeyState
|
|
||||||
if (!publicKeyState) {
|
|
||||||
throw new Error('xeapi publicKeyState is required')
|
|
||||||
}
|
|
||||||
const activeSessionKey = options.sessionKey
|
|
||||||
? Buffer.from(String(options.sessionKey))
|
|
||||||
: null
|
|
||||||
const activeSessionId = options.sessionId || ''
|
|
||||||
const dynamicKey = activeSessionKey || crypto.randomBytes(16)
|
|
||||||
const plaintext = Buffer.from(buildXeapiPlaintext(uri, data, options))
|
|
||||||
|
|
||||||
const b = aesEcbEncrypt(
|
|
||||||
dynamicKey,
|
|
||||||
xeapiMidTransform(aesEcbEncrypt(xeapiStaticKey, plaintext)),
|
|
||||||
)
|
|
||||||
const s = xeapiEncryptS(dynamicKey, publicKeyState, options.os || 'android')
|
|
||||||
const r = aesEcbEncrypt(
|
|
||||||
xeapiStaticKey,
|
|
||||||
Buffer.from(
|
|
||||||
`${publicKeyState.version}|${activeSessionKey ? activeSessionId : ''}`,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
B: b.toString('base64'),
|
|
||||||
S: s.toString('base64'),
|
|
||||||
R: r.toString('base64'),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const xeapiResDecrypt = (body) => {
|
|
||||||
const decrypted = aesEcbDecrypt(eapiKey, body)
|
|
||||||
const plaintext =
|
|
||||||
decrypted[0] === 0x1f && decrypted[1] === 0x8b
|
|
||||||
? zlib.gunzipSync(decrypted)
|
|
||||||
: decrypted
|
|
||||||
return JSON.parse(plaintext.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
const xeapiDecryptPublicKey = (encryptedData) => {
|
|
||||||
return JSON.parse(
|
|
||||||
aesEcbDecrypt(
|
|
||||||
xeapiStaticKey,
|
|
||||||
Buffer.from(encryptedData, 'base64'),
|
|
||||||
).toString(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
weapi,
|
weapi,
|
||||||
linuxapi,
|
linuxapi,
|
||||||
eapi,
|
eapi,
|
||||||
xeapi,
|
|
||||||
decrypt,
|
decrypt,
|
||||||
aesEncrypt,
|
aesEncrypt,
|
||||||
aesDecrypt,
|
aesDecrypt,
|
||||||
eapiReqDecrypt,
|
eapiReqDecrypt,
|
||||||
eapiResDecrypt,
|
eapiResDecrypt,
|
||||||
xeapiSign,
|
|
||||||
xeapiResDecrypt,
|
|
||||||
xeapiDecryptPublicKey,
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -62,9 +62,9 @@ const chinaIPRanges = (function loadChinaIPRanges() {
|
|||||||
// attach total for convenience
|
// attach total for convenience
|
||||||
arr.totalCount = total
|
arr.totalCount = total
|
||||||
|
|
||||||
// logger.info(
|
logger.info(
|
||||||
// `Loaded ${arr.length} Chinese IP ranges from china_ip_ranges.txt, total ${total} IPs`,
|
`Loaded ${arr.length} Chinese IP ranges from china_ip_ranges.txt, total ${total} IPs`,
|
||||||
// )
|
)
|
||||||
return arr
|
return arr
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to load china_ip_ranges.txt:', error.message)
|
logger.error('Failed to load china_ip_ranges.txt:', error.message)
|
||||||
|
|||||||
481
util/ncbl.js
481
util/ncbl.js
@ -1,481 +0,0 @@
|
|||||||
// NCBL 加密工具 —— 复制自 @netease-report-listen-song
|
|
||||||
// 提供 ChaCha20 / RSA-256 / NCBL 加密 / PLV+PLD 构建 / 设备上下文提取
|
|
||||||
|
|
||||||
const crypto = require('crypto')
|
|
||||||
const zlib = require('zlib')
|
|
||||||
const axios = require('axios')
|
|
||||||
const { APP_CONF } = require('./config.json')
|
|
||||||
const DOMAIN3 = APP_CONF.clDomian3
|
|
||||||
|
|
||||||
// ---- ChaCha20 纯 JS 实现 ----
|
|
||||||
const SIGMA = [0x61707865, 0x3320646e, 0x79622d32, 0x6b206574]
|
|
||||||
|
|
||||||
const rotl = (x, n) => ((x << n) | (x >>> (32 - n))) >>> 0
|
|
||||||
|
|
||||||
const quarterRound = (s, a, b, c, d) => {
|
|
||||||
s[a] = (s[a] + s[b]) >>> 0
|
|
||||||
s[d] ^= s[a]
|
|
||||||
s[d] = rotl(s[d], 16)
|
|
||||||
s[c] = (s[c] + s[d]) >>> 0
|
|
||||||
s[b] ^= s[c]
|
|
||||||
s[b] = rotl(s[b], 12)
|
|
||||||
s[a] = (s[a] + s[b]) >>> 0
|
|
||||||
s[d] ^= s[a]
|
|
||||||
s[d] = rotl(s[d], 8)
|
|
||||||
s[c] = (s[c] + s[d]) >>> 0
|
|
||||||
s[b] ^= s[c]
|
|
||||||
s[b] = rotl(s[b], 7)
|
|
||||||
}
|
|
||||||
|
|
||||||
const chachaBlock = (key, counter, nonce) => {
|
|
||||||
const state = new Uint32Array(16)
|
|
||||||
state[0] = SIGMA[0]
|
|
||||||
state[1] = SIGMA[1]
|
|
||||||
state[2] = SIGMA[2]
|
|
||||||
state[3] = SIGMA[3]
|
|
||||||
for (let i = 0; i < 8; i++) state[4 + i] = key.readUInt32LE(i * 4)
|
|
||||||
state[12] = counter >>> 0
|
|
||||||
state[13] = nonce.readUInt32LE(0)
|
|
||||||
state[14] = nonce.readUInt32LE(4)
|
|
||||||
state[15] = nonce.readUInt32LE(8)
|
|
||||||
|
|
||||||
const work = state.slice()
|
|
||||||
for (let i = 0; i < 10; i++) {
|
|
||||||
quarterRound(work, 0, 4, 8, 12)
|
|
||||||
quarterRound(work, 1, 5, 9, 13)
|
|
||||||
quarterRound(work, 2, 6, 10, 14)
|
|
||||||
quarterRound(work, 3, 7, 11, 15)
|
|
||||||
quarterRound(work, 0, 5, 10, 15)
|
|
||||||
quarterRound(work, 1, 6, 11, 12)
|
|
||||||
quarterRound(work, 2, 7, 8, 13)
|
|
||||||
quarterRound(work, 3, 4, 9, 14)
|
|
||||||
}
|
|
||||||
|
|
||||||
const out = Buffer.allocUnsafe(64)
|
|
||||||
for (let i = 0; i < 16; i++)
|
|
||||||
out.writeUInt32LE((work[i] + state[i]) >>> 0, i * 4)
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
const chacha20 = (key, counter, nonce, data) => {
|
|
||||||
const out = Buffer.allocUnsafe(data.length)
|
|
||||||
for (let off = 0; off < data.length; off += 64) {
|
|
||||||
const ks = chachaBlock(key, (counter + (off >>> 6)) >>> 0, nonce)
|
|
||||||
const end = Math.min(off + 64, data.length)
|
|
||||||
for (let i = off; i < end; i++) out[i] = data[i] ^ ks[i - off]
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- RSA-256 Key Wrap (网易 NCBL 专用) ----
|
|
||||||
// 256-bit 模数 N 已被因式分解,故可直接计算私钥
|
|
||||||
const RSA_N =
|
|
||||||
0xfd90bd466ff9bc8a3fec2fbcf263b90d5c564879fa5d7aab89b31c1d5cb4139dn
|
|
||||||
const RSA_E = 65537n
|
|
||||||
|
|
||||||
const beToBig = (buf) => {
|
|
||||||
let n = 0n
|
|
||||||
for (const b of buf) n = (n << 8n) | BigInt(b)
|
|
||||||
return n
|
|
||||||
}
|
|
||||||
|
|
||||||
const bigToBe = (n, len) => {
|
|
||||||
const out = Buffer.alloc(len)
|
|
||||||
for (let i = len - 1; i >= 0; i--) {
|
|
||||||
out[i] = Number(n & 0xffn)
|
|
||||||
n >>= 8n
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
const modPow = (base, exp, mod) => {
|
|
||||||
let result = 1n
|
|
||||||
base %= mod
|
|
||||||
while (exp > 0n) {
|
|
||||||
if (exp & 1n) result = (result * base) % mod
|
|
||||||
base = (base * base) % mod
|
|
||||||
exp >>= 1n
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
const rsaWrap = (keyA) => {
|
|
||||||
return bigToBe(modPow(beToBig(keyA), RSA_E, RSA_N), 32)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- NCBL 加密格式 ----
|
|
||||||
const MAGIC = Buffer.from('NCBL', 'ascii')
|
|
||||||
const NCBL_VERSION = 3
|
|
||||||
const HEADER_FIXED_LEN = 70
|
|
||||||
const META_BLOCK_TYPE = 0x4343
|
|
||||||
const DEFAULT_MAX_FRAME = 0x8000
|
|
||||||
|
|
||||||
const getCompress = () => {
|
|
||||||
if (typeof zlib.zstdCompressSync === 'function') {
|
|
||||||
return { compress: (buf) => zlib.zstdCompressSync(buf), name: 'zstd' }
|
|
||||||
}
|
|
||||||
return { compress: (buf) => zlib.gzipSync(buf), name: 'gzip' }
|
|
||||||
}
|
|
||||||
|
|
||||||
const encryptNCBL = (meta, body, opts = {}) => {
|
|
||||||
const metaBuf = Buffer.isBuffer(meta) ? meta : Buffer.from(meta, 'utf-8')
|
|
||||||
const bodyBuf = Buffer.isBuffer(body) ? body : Buffer.from(body, 'utf-8')
|
|
||||||
const maxFrame = opts.maxFrame || DEFAULT_MAX_FRAME
|
|
||||||
|
|
||||||
const keyA = opts.keyA || crypto.randomBytes(32)
|
|
||||||
if (keyA[0] >= 0xa3) keyA[0] = 0xa2
|
|
||||||
|
|
||||||
const keyB = rsaWrap(keyA)
|
|
||||||
|
|
||||||
const uuid = opts.uuid || crypto.randomBytes(16)
|
|
||||||
if (!opts.uuid) {
|
|
||||||
uuid[6] = (uuid[6] & 0x0f) | 0x40
|
|
||||||
uuid[8] = (uuid[8] & 0x3f) | 0x80
|
|
||||||
}
|
|
||||||
const nonce = uuid.subarray(0, 12)
|
|
||||||
const counter = uuid.readUInt32LE(12) >>> 2
|
|
||||||
const baseSeq = opts.baseSeq || crypto.randomBytes(2).readUInt16LE(0)
|
|
||||||
|
|
||||||
const metaCipher = chacha20(keyB, counter, nonce, metaBuf)
|
|
||||||
const metaBlock = Buffer.concat([
|
|
||||||
(() => {
|
|
||||||
const h = Buffer.allocUnsafe(4)
|
|
||||||
h.writeUInt16LE(META_BLOCK_TYPE, 0)
|
|
||||||
h.writeUInt16LE(metaCipher.length, 2)
|
|
||||||
return h
|
|
||||||
})(),
|
|
||||||
metaCipher,
|
|
||||||
])
|
|
||||||
const headerLen = HEADER_FIXED_LEN + metaBlock.length
|
|
||||||
|
|
||||||
const { compress } = getCompress()
|
|
||||||
const compressed = compress(bodyBuf)
|
|
||||||
|
|
||||||
const frames = []
|
|
||||||
let seq = baseSeq
|
|
||||||
for (let off = 0; off < compressed.length || off === 0; off += maxFrame) {
|
|
||||||
const slice = compressed.subarray(off, off + maxFrame)
|
|
||||||
const cipher = chacha20(keyA, counter, nonce, slice)
|
|
||||||
const head = Buffer.allocUnsafe(6)
|
|
||||||
head.writeUInt16LE(cipher.length, 0)
|
|
||||||
head.writeUInt32LE(seq >>> 0, 2)
|
|
||||||
frames.push(head, cipher)
|
|
||||||
seq++
|
|
||||||
if (compressed.length === 0) break
|
|
||||||
}
|
|
||||||
|
|
||||||
const trailing = Buffer.concat(frames)
|
|
||||||
const frameCount = seq - baseSeq
|
|
||||||
|
|
||||||
const header = Buffer.alloc(HEADER_FIXED_LEN)
|
|
||||||
MAGIC.copy(header, 0)
|
|
||||||
header.writeUInt32LE(NCBL_VERSION, 4)
|
|
||||||
header.writeUInt16LE(headerLen, 8)
|
|
||||||
uuid.copy(header, 10)
|
|
||||||
keyB.copy(header, 26)
|
|
||||||
header.writeUInt32LE(baseSeq >>> 0, 58)
|
|
||||||
header.writeUInt32LE((baseSeq + frameCount - 1) >>> 0, 62)
|
|
||||||
header.writeUInt32LE(trailing.length, 66)
|
|
||||||
|
|
||||||
return Buffer.concat([header, metaBlock, trailing])
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- 日志记录格式 ----
|
|
||||||
const FIELD_SEP = '\x01'
|
|
||||||
|
|
||||||
const buildRecord = ({ time, action, data }) => {
|
|
||||||
const json = typeof data === 'string' ? data : JSON.stringify(data)
|
|
||||||
return [time, action, json].join(FIELD_SEP)
|
|
||||||
}
|
|
||||||
|
|
||||||
const buildRecords = (records) => records.map(buildRecord).join('')
|
|
||||||
|
|
||||||
// ---- PLV / PLD 构建器 (桌面客户端格式) ----
|
|
||||||
const buildPlv = (ctx, song, source) => {
|
|
||||||
const now = Date.now()
|
|
||||||
const addRefer = `[F:63][${now}#933#${ctx.app.version}#${ctx.app.versionCode}#c9156c3][e][2][23][cell_pc_songlist_song:2|page_pc_songlist_songflow|page_mine_like_music][${song.id}:song:x:x|:::|${source.id}:list::]`
|
|
||||||
const multiRefers = [
|
|
||||||
'[F:26][s][18][_ai]',
|
|
||||||
'[F:26][s][12][_ai]',
|
|
||||||
`[F:63][${now}#933#${ctx.app.version}#${ctx.app.versionCode}#c9156c3][e][2][8][cell_pc_main_tab_entrance:6|page_pc_main_tab][我喜欢的音乐:spm::|:::]`,
|
|
||||||
'[F:26][s][5][_ai]',
|
|
||||||
'[F:26][s][0][_ai]',
|
|
||||||
]
|
|
||||||
|
|
||||||
return {
|
|
||||||
mode: 'circulation',
|
|
||||||
download: 0,
|
|
||||||
alg: '',
|
|
||||||
status: 'front',
|
|
||||||
id: String(song.id),
|
|
||||||
bitrate: song.bitrate,
|
|
||||||
type: 'song',
|
|
||||||
is_listentogether: 0,
|
|
||||||
source: source.name,
|
|
||||||
is_heart: 0,
|
|
||||||
resource_ratio: '',
|
|
||||||
resource_time: song.time,
|
|
||||||
musiceffect_id: '',
|
|
||||||
app_mode: 2,
|
|
||||||
bitrate_level: song.level,
|
|
||||||
_addrefer: addRefer,
|
|
||||||
_multirefers: multiRefers,
|
|
||||||
vipType: ctx.auth.vipType,
|
|
||||||
fee: 1,
|
|
||||||
file: 4,
|
|
||||||
rightSource: 0,
|
|
||||||
sourceId: source.id,
|
|
||||||
sourcetype: source.type,
|
|
||||||
libra_abt: '',
|
|
||||||
channel: ctx.app.channel,
|
|
||||||
curStartChannel: '',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const buildPld = (ctx, song, source, played) => {
|
|
||||||
const now = Date.now()
|
|
||||||
const addRefer = `[F:63][${now}#616#${ctx.app.version}#${ctx.app.versionCode}#c9156c3][e][2][92][btn_pc_cover_play|cell_pc_songlist_song:6|page_pc_songlist_songflow|page_mine_like_music][:::|${song.id}:song:x:x|:::|${source.id}:list::]`
|
|
||||||
const multiRefers = [
|
|
||||||
'[F:26][s][87][_ai]',
|
|
||||||
'[F:26][s][81][_ai]',
|
|
||||||
'[F:26][s][75][_ai]',
|
|
||||||
'[F:26][s][69][_ai]',
|
|
||||||
'[F:26][s][63][_ai]',
|
|
||||||
]
|
|
||||||
|
|
||||||
return {
|
|
||||||
mode: 'circulation',
|
|
||||||
download: 0,
|
|
||||||
alg: '',
|
|
||||||
status: 'front',
|
|
||||||
id: String(song.id),
|
|
||||||
time: played,
|
|
||||||
type: 'song',
|
|
||||||
is_listentogether: 0,
|
|
||||||
source: source.name,
|
|
||||||
is_heart: 0,
|
|
||||||
realtime: played,
|
|
||||||
resource_ratio: '',
|
|
||||||
resource_time: song.time,
|
|
||||||
musiceffect_id: '1001',
|
|
||||||
app_mode: 1,
|
|
||||||
lyriceffect: 'default',
|
|
||||||
displayMode: 'classic',
|
|
||||||
bitrate: song.bitrate,
|
|
||||||
bitrate_level: song.level,
|
|
||||||
_addrefer: addRefer,
|
|
||||||
_multirefers: multiRefers,
|
|
||||||
vipType: ctx.auth.vipType,
|
|
||||||
fee: 8,
|
|
||||||
file: 4,
|
|
||||||
rightSource: 0,
|
|
||||||
sourceId: source.id,
|
|
||||||
sourcetype: source.type,
|
|
||||||
end: 'interrupt',
|
|
||||||
libra_abt: '',
|
|
||||||
channel: ctx.app.channel,
|
|
||||||
curStartChannel: '',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Cookie → 设备/认证上下文 转换 ----
|
|
||||||
const extractContext = (cookieObj) => {
|
|
||||||
return {
|
|
||||||
app: {
|
|
||||||
id: cookieObj.appid || '',
|
|
||||||
urs: '',
|
|
||||||
pid: '',
|
|
||||||
nsm: cookieObj.WEVNSM || '1.0.0',
|
|
||||||
cid:
|
|
||||||
cookieObj.WNMCID ||
|
|
||||||
`${crypto.randomBytes(3).toString('hex')}.${Date.now()}.01.0`,
|
|
||||||
channel: cookieObj.channel || 'netease',
|
|
||||||
version: cookieObj.appver || '3.1.35',
|
|
||||||
versionCode: cookieObj.versioncode || '205293',
|
|
||||||
buildCode: cookieObj.buildver || '',
|
|
||||||
buildType: 'release',
|
|
||||||
packageId: '',
|
|
||||||
},
|
|
||||||
device: {
|
|
||||||
id: cookieObj.deviceId || cookieObj.sDeviceId || '',
|
|
||||||
ti: cookieObj.NMTID || '',
|
|
||||||
sign: cookieObj.clientSign || '',
|
|
||||||
model: cookieObj.mode || cookieObj.mobilename || '',
|
|
||||||
nnid: cookieObj._ntes_nnid || ',',
|
|
||||||
nuid: cookieObj._ntes_nuid || '',
|
|
||||||
csrf: cookieObj.__csrf || '',
|
|
||||||
systemType: cookieObj.os || 'pc',
|
|
||||||
systemVersion:
|
|
||||||
cookieObj.osver ||
|
|
||||||
'Microsoft-Windows-10-Professional-build-19045-64bit',
|
|
||||||
},
|
|
||||||
auth: {
|
|
||||||
id: cookieObj.uid || '',
|
|
||||||
token: cookieObj.MUSIC_U || '',
|
|
||||||
sessionId: cookieObj['JSESSIONID-WYYY'] || '',
|
|
||||||
vipType: cookieObj.vipType || '',
|
|
||||||
},
|
|
||||||
startTime: Date.now(),
|
|
||||||
processId: Math.floor(Math.random() * 90000) + 10000,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 从 query.cookie 解析 cookie 对象
|
|
||||||
// 支持字符串 ("key=val; key2=val2") 或已解析的对象
|
|
||||||
// 注意:不做 URL decode!MUSIC_U 中的 '+' 会被 decodeURIComponent 错误地转为空格
|
|
||||||
const parseCookie = (cookie) => {
|
|
||||||
if (typeof cookie === 'object' && cookie !== null) return cookie
|
|
||||||
if (typeof cookie === 'string') {
|
|
||||||
const obj = {}
|
|
||||||
cookie.split(';').forEach((part) => {
|
|
||||||
const idx = part.indexOf('=')
|
|
||||||
if (idx > 0) {
|
|
||||||
const key = part.substring(0, idx).trim()
|
|
||||||
const val = part.substring(idx + 1).trim()
|
|
||||||
if (key) obj[key] = val
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return obj
|
|
||||||
}
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- 辅助函数 ----
|
|
||||||
const randomUUID = () => {
|
|
||||||
if (typeof crypto.randomUUID === 'function') {
|
|
||||||
return crypto.randomUUID().replace(/-/g, '')
|
|
||||||
}
|
|
||||||
return 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'.replace(/x/g, () =>
|
|
||||||
Math.floor(Math.random() * 16).toString(16),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const randomHex = (len) => crypto.randomBytes(len / 2).toString('hex')
|
|
||||||
|
|
||||||
// ---- HTTP 上传工具 (NCBL 专用) ----
|
|
||||||
const buildMultipart = (payload) => {
|
|
||||||
const boundary = randomUUID()
|
|
||||||
const fileName = `op_${Math.floor(Math.random() * 90000) + 10000}_0_${Math.floor(Math.random() * 4294967295) + 1}`
|
|
||||||
|
|
||||||
const CRLF = '\r\n'
|
|
||||||
const headerLines = [
|
|
||||||
`--${boundary}`,
|
|
||||||
`Content-Disposition: form-data; name="file"; filename="${fileName}"`,
|
|
||||||
'Content-Type: multipart/form-data',
|
|
||||||
'',
|
|
||||||
'',
|
|
||||||
].join(CRLF)
|
|
||||||
const footer = `${CRLF}--${boundary}--${CRLF}`
|
|
||||||
|
|
||||||
return {
|
|
||||||
boundary,
|
|
||||||
fileName,
|
|
||||||
multipartBody: Buffer.concat([
|
|
||||||
Buffer.from(headerLines, 'utf-8'),
|
|
||||||
payload,
|
|
||||||
Buffer.from(footer, 'utf-8'),
|
|
||||||
]),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const buildCookieStr = (ctx) => {
|
|
||||||
const parts = [
|
|
||||||
`JSESSIONID-WYYY=${ctx.auth.sessionId}`,
|
|
||||||
`MUSIC_U=${ctx.auth.token}`,
|
|
||||||
`NMTID=${ctx.device.ti}`,
|
|
||||||
`WEVNSM=${ctx.app.nsm}`,
|
|
||||||
`WNMCID=${ctx.app.cid}`,
|
|
||||||
`__csrf=${ctx.device.csrf}`,
|
|
||||||
`__remember_me=true`,
|
|
||||||
`_iuqxldmzr_=33`,
|
|
||||||
`_ntes_nnid=${ctx.device.nnid}`,
|
|
||||||
`_ntes_nuid=${ctx.device.nuid}`,
|
|
||||||
`appver=${ctx.app.version}.${ctx.app.versionCode}`,
|
|
||||||
`channel=${ctx.app.channel}`,
|
|
||||||
`clientSign=${ctx.device.sign}`,
|
|
||||||
`deviceId=${ctx.device.id}`,
|
|
||||||
`mode=${ctx.device.model}`,
|
|
||||||
`ntes_kaola_ad=1`,
|
|
||||||
`os=${ctx.device.systemType}`,
|
|
||||||
`osver=${ctx.device.systemVersion}`,
|
|
||||||
]
|
|
||||||
return parts.join('; ')
|
|
||||||
}
|
|
||||||
|
|
||||||
const buildMetaJson = (ctx) =>
|
|
||||||
JSON.stringify({
|
|
||||||
'JSESSIONID-WYYY': ctx.auth.sessionId,
|
|
||||||
MUSIC_U: ctx.auth.token,
|
|
||||||
NMTID: ctx.device.ti,
|
|
||||||
WEVNSM: ctx.app.nsm,
|
|
||||||
WNMCID: ctx.app.cid,
|
|
||||||
__csrf: ctx.device.csrf,
|
|
||||||
_iuqxldmzr_: '33',
|
|
||||||
_ntes_nnid: ctx.device.nnid,
|
|
||||||
_ntes_nuid: ctx.device.nuid,
|
|
||||||
appver: `${ctx.app.version}.${ctx.app.versionCode}`,
|
|
||||||
channel: ctx.app.channel,
|
|
||||||
clientSign: ctx.device.sign,
|
|
||||||
deviceId: ctx.device.id,
|
|
||||||
mode: ctx.device.model,
|
|
||||||
ntes_kaola_ad: '1',
|
|
||||||
os: ctx.device.systemType,
|
|
||||||
osver: ctx.device.systemVersion,
|
|
||||||
})
|
|
||||||
|
|
||||||
const doUpload = async (ctx, metaJson, body, cookieStr, label) => {
|
|
||||||
const uploadUrl = DOMAIN3 + '/api/clientlog/encrypt/upload?multiupload=true'
|
|
||||||
|
|
||||||
const payload = encryptNCBL(metaJson, body)
|
|
||||||
const { boundary, fileName, multipartBody } = buildMultipart(payload)
|
|
||||||
|
|
||||||
const resp = await axios({
|
|
||||||
method: 'POST',
|
|
||||||
url: uploadUrl,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': `multipart/form-data; boundary=${boundary}`,
|
|
||||||
Referer: 'https://music.163.com/di',
|
|
||||||
'User-Agent': `Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Safari/537.36 Chrome/91.0.4472.164 NeteaseMusicDesktop/${ctx.app.version}`,
|
|
||||||
'Accept-Encoding': 'gzip,deflate',
|
|
||||||
'Accept-Language': 'zh-CN,zh;q=0.8',
|
|
||||||
Cookie: cookieStr,
|
|
||||||
},
|
|
||||||
data: multipartBody,
|
|
||||||
maxBodyLength: 10 * 1024 * 1024,
|
|
||||||
timeout: 15000,
|
|
||||||
validateStatus: () => true,
|
|
||||||
})
|
|
||||||
|
|
||||||
const respBody = resp.data
|
|
||||||
const code = respBody?.code
|
|
||||||
const success =
|
|
||||||
code === 200 && respBody?.data?.successfiles?.includes?.(fileName)
|
|
||||||
|
|
||||||
return { success, fileName, payload, respBody }
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- 导出 ----
|
|
||||||
module.exports = {
|
|
||||||
chacha20,
|
|
||||||
rsaWrap,
|
|
||||||
encryptNCBL,
|
|
||||||
getCompress,
|
|
||||||
MAGIC,
|
|
||||||
NCBL_VERSION,
|
|
||||||
HEADER_FIXED_LEN,
|
|
||||||
META_BLOCK_TYPE,
|
|
||||||
DEFAULT_MAX_FRAME,
|
|
||||||
buildRecord,
|
|
||||||
buildRecords,
|
|
||||||
FIELD_SEP,
|
|
||||||
buildPlv,
|
|
||||||
buildPld,
|
|
||||||
extractContext,
|
|
||||||
parseCookie,
|
|
||||||
randomUUID,
|
|
||||||
randomHex,
|
|
||||||
buildMultipart,
|
|
||||||
buildCookieStr,
|
|
||||||
buildMetaJson,
|
|
||||||
doUpload,
|
|
||||||
}
|
|
||||||
@ -5,10 +5,7 @@ const createOption = (query, crypto = '') => {
|
|||||||
ua: query.ua || '',
|
ua: query.ua || '',
|
||||||
proxy: query.proxy,
|
proxy: query.proxy,
|
||||||
realIP: query.realIP,
|
realIP: query.realIP,
|
||||||
randomCNIP:
|
randomCNIP: process.env.ENABLE_RANDOM_CN_IP === 'true' ? !['false', false].includes(query.randomCNIP) : ['true', true].includes(query.randomCNIP),
|
||||||
process.env.ENABLE_RANDOM_CN_IP === 'true'
|
|
||||||
? !['false', false].includes(query.randomCNIP)
|
|
||||||
: ['true', true].includes(query.randomCNIP),
|
|
||||||
e_r: query.e_r || undefined,
|
e_r: query.e_r || undefined,
|
||||||
domain: query.domain || '',
|
domain: query.domain || '',
|
||||||
checkToken: query.checkToken || false,
|
checkToken: query.checkToken || false,
|
||||||
|
|||||||
100
util/request.js
100
util/request.js
@ -2,7 +2,6 @@
|
|||||||
const encrypt = require('./crypto')
|
const encrypt = require('./crypto')
|
||||||
const CryptoJS = require('crypto-js')
|
const CryptoJS = require('crypto-js')
|
||||||
const { default: axios } = require('axios')
|
const { default: axios } = require('axios')
|
||||||
const logger = require('./logger')
|
|
||||||
const { PacProxyAgent } = require('pac-proxy-agent')
|
const { PacProxyAgent } = require('pac-proxy-agent')
|
||||||
const http = require('http')
|
const http = require('http')
|
||||||
const https = require('https')
|
const https = require('https')
|
||||||
@ -24,20 +23,6 @@ const anonymous_token = fs.readFileSync(
|
|||||||
path.resolve(tmpPath, './anonymous_token'),
|
path.resolve(tmpPath, './anonymous_token'),
|
||||||
'utf-8',
|
'utf-8',
|
||||||
)
|
)
|
||||||
const xeapiPublicKeyPath = path.resolve(tmpPath, './xeapi_public_key')
|
|
||||||
let xeapi_public_key = null
|
|
||||||
const loadXeapiPublicKey = () => {
|
|
||||||
if (!xeapi_public_key && fs.existsSync(xeapiPublicKeyPath)) {
|
|
||||||
try {
|
|
||||||
xeapi_public_key = JSON.parse(
|
|
||||||
fs.readFileSync(xeapiPublicKeyPath, 'utf-8'),
|
|
||||||
)
|
|
||||||
} catch (error) {
|
|
||||||
console.log('[ERR]', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return xeapi_public_key
|
|
||||||
}
|
|
||||||
|
|
||||||
// 预先绑定常用函数和常量
|
// 预先绑定常用函数和常量
|
||||||
const floor = Math.floor
|
const floor = Math.floor
|
||||||
@ -88,12 +73,6 @@ const osMap = {
|
|||||||
osver: '16.2',
|
osver: '16.2',
|
||||||
channel: 'distribution',
|
channel: 'distribution',
|
||||||
},
|
},
|
||||||
osx: {
|
|
||||||
os: 'osx',
|
|
||||||
appver: '3.1.10.5100',
|
|
||||||
osver: '15.5',
|
|
||||||
channel: 'netease',
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 预先定义userAgentMap
|
// 预先定义userAgentMap
|
||||||
@ -116,13 +95,9 @@ const userAgentMap = {
|
|||||||
// 预先定义常量
|
// 预先定义常量
|
||||||
const DOMAIN = APP_CONF.domain
|
const DOMAIN = APP_CONF.domain
|
||||||
const API_DOMAIN = APP_CONF.apiDomain
|
const API_DOMAIN = APP_CONF.apiDomain
|
||||||
const XEAPI_DOMAIN = APP_CONF.xeapiDomain
|
|
||||||
const ENCRYPT_RESPONSE = APP_CONF.encryptResponse
|
const ENCRYPT_RESPONSE = APP_CONF.encryptResponse
|
||||||
const SPECIAL_STATUS_CODES = new Set([201, 302, 400, 502, 800, 801, 802, 803])
|
const SPECIAL_STATUS_CODES = new Set([201, 302, 400, 502, 800, 801, 802, 803])
|
||||||
|
|
||||||
let xeapiSessionId = ''
|
|
||||||
let xeapiSessionKey = ''
|
|
||||||
|
|
||||||
// chooseUserAgent函数
|
// chooseUserAgent函数
|
||||||
const chooseUserAgent = (crypto, uaType = 'pc') => {
|
const chooseUserAgent = (crypto, uaType = 'pc') => {
|
||||||
return (userAgentMap[crypto] && userAgentMap[crypto][uaType]) || ''
|
return (userAgentMap[crypto] && userAgentMap[crypto][uaType]) || ''
|
||||||
@ -241,52 +216,6 @@ const createRequest = (uri, data, options) => {
|
|||||||
url = (options.domain || DOMAIN) + '/api/linux/forward'
|
url = (options.domain || DOMAIN) + '/api/linux/forward'
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'xeapi':
|
|
||||||
const xeapiPublicKey = loadXeapiPublicKey()
|
|
||||||
if (!xeapiPublicKey) {
|
|
||||||
throw new Error('xeapi public key is missing')
|
|
||||||
}
|
|
||||||
const xeapiOs = cookie.os === 'android' ? cookie.os : 'android'
|
|
||||||
const xeapiAppver =
|
|
||||||
cookie.os === 'android' && cookie.appver ? cookie.appver : '9.1.65'
|
|
||||||
const xeapiOsver =
|
|
||||||
cookie.os === 'android' && cookie.osver ? cookie.osver : '16'
|
|
||||||
const xeapiBuildver = cookie.buildver || now().toString().substr(0, 10)
|
|
||||||
headers['User-Agent'] = options.ua || chooseUserAgent('api', 'android')
|
|
||||||
headers['X-Client-Enc-State'] = 'ENCRYPTED'
|
|
||||||
headers['x-aeapi'] = true
|
|
||||||
headers['content-type'] =
|
|
||||||
'application/x-www-form-urlencoded;charset=utf-8'
|
|
||||||
headers['x-deviceid'] = cookie.deviceId
|
|
||||||
headers['x-os'] = xeapiOs
|
|
||||||
headers['x-osver'] = xeapiOsver
|
|
||||||
headers['x-appver'] = xeapiAppver
|
|
||||||
headers['x-sdeviceid'] = cookie.sDeviceId || cookie.deviceId
|
|
||||||
headers['x-buildver'] = xeapiBuildver
|
|
||||||
if (cookie.MUSIC_U) headers['x-music-u'] = cookie.MUSIC_U
|
|
||||||
const xeapiCookie = {
|
|
||||||
...cookie,
|
|
||||||
os: xeapiOs,
|
|
||||||
osver: xeapiOsver,
|
|
||||||
appver: xeapiAppver,
|
|
||||||
buildver: xeapiBuildver,
|
|
||||||
deviceId: cookie.deviceId,
|
|
||||||
sDeviceId: cookie.sDeviceId || cookie.deviceId,
|
|
||||||
}
|
|
||||||
headers['Cookie'] = cookieObjToString(xeapiCookie)
|
|
||||||
url = (options.domain || XEAPI_DOMAIN) + '/xeapi/' + uri.substr(5)
|
|
||||||
encryptData = encrypt.xeapi(uri, data, {
|
|
||||||
...options,
|
|
||||||
publicKeyState: xeapiPublicKey,
|
|
||||||
sessionId: xeapiSessionId,
|
|
||||||
sessionKey: xeapiSessionKey,
|
|
||||||
appver: xeapiAppver,
|
|
||||||
deviceId: cookie.deviceId,
|
|
||||||
os: xeapiOs,
|
|
||||||
uid: cookie.uid || cookie.userId || '',
|
|
||||||
})
|
|
||||||
break
|
|
||||||
|
|
||||||
case 'eapi':
|
case 'eapi':
|
||||||
case 'api':
|
case 'api':
|
||||||
// header创建
|
// header创建
|
||||||
@ -312,11 +241,7 @@ const createRequest = (uri, data, options) => {
|
|||||||
if (cookie.MUSIC_A) header['MUSIC_A'] = cookie.MUSIC_A
|
if (cookie.MUSIC_A) header['MUSIC_A'] = cookie.MUSIC_A
|
||||||
|
|
||||||
headers['Cookie'] = createHeaderCookie(header)
|
headers['Cookie'] = createHeaderCookie(header)
|
||||||
headers['User-Agent'] =
|
headers['User-Agent'] = options.ua || chooseUserAgent('api', 'iphone')
|
||||||
options.ua ||
|
|
||||||
(cookie.os === 'osx'
|
|
||||||
? 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36'
|
|
||||||
: chooseUserAgent('api', 'iphone'))
|
|
||||||
|
|
||||||
if (crypto === 'eapi') {
|
if (crypto === 'eapi') {
|
||||||
// headers['x-aeapi'] = true // 服务器会使用gzip压缩返回值
|
// headers['x-aeapi'] = true // 服务器会使用gzip压缩返回值
|
||||||
@ -334,6 +259,7 @@ const createRequest = (uri, data, options) => {
|
|||||||
console.log('[ERR]', 'Unknown Crypto:', crypto)
|
console.log('[ERR]', 'Unknown Crypto:', crypto)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
// console.log(url);
|
||||||
// settings创建
|
// settings创建
|
||||||
let settings = {
|
let settings = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@ -346,8 +272,7 @@ const createRequest = (uri, data, options) => {
|
|||||||
|
|
||||||
// 使用返回值加密
|
// 使用返回值加密
|
||||||
const use_e_r = (crypto === 'eapi' || crypto === 'weapi') && data.e_r
|
const use_e_r = (crypto === 'eapi' || crypto === 'weapi') && data.e_r
|
||||||
const use_xeapi = crypto === 'xeapi'
|
if (use_e_r) {
|
||||||
if (use_e_r || use_xeapi) {
|
|
||||||
settings.encoding = null
|
settings.encoding = null
|
||||||
settings.responseType = 'arraybuffer'
|
settings.responseType = 'arraybuffer'
|
||||||
}
|
}
|
||||||
@ -394,25 +319,8 @@ const createRequest = (uri, data, options) => {
|
|||||||
x.replace(/\s*Domain=[^(;|$)]+;*/, ''),
|
x.replace(/\s*Domain=[^(;|$)]+;*/, ''),
|
||||||
)
|
)
|
||||||
|
|
||||||
// debug: 统一注释块,需要时取消注释查看请求/返回的原始密文
|
|
||||||
|
|
||||||
// logger.debug(`[${crypto}]`, uri)
|
|
||||||
// logger.debug(`[${crypto}] encrypted data:`, JSON.stringify(encryptData))
|
|
||||||
// logger.debug(
|
|
||||||
// `[RAW] [${crypto}]`,
|
|
||||||
// use_xeapi
|
|
||||||
// ? Buffer.from(body).toString('base64')
|
|
||||||
// : body.toString('hex').toUpperCase(),
|
|
||||||
// )
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (use_xeapi) {
|
if (use_e_r) {
|
||||||
if (res.headers['x-encr-ssid'] && res.headers['x-encr-sskey']) {
|
|
||||||
xeapiSessionId = res.headers['x-encr-ssid']
|
|
||||||
xeapiSessionKey = res.headers['x-encr-sskey']
|
|
||||||
}
|
|
||||||
answer.body = encrypt.xeapiResDecrypt(Buffer.from(body))
|
|
||||||
} else if (use_e_r) {
|
|
||||||
answer.body = encrypt.eapiResDecrypt(
|
answer.body = encrypt.eapiResDecrypt(
|
||||||
body.toString('hex').toUpperCase(),
|
body.toString('hex').toUpperCase(),
|
||||||
headers['x-aeapi'],
|
headers['x-aeapi'],
|
||||||
|
|||||||
@ -1,24 +0,0 @@
|
|||||||
const registerXeapiKey = require('../module/register_xeapikey')
|
|
||||||
|
|
||||||
const getXeapiPublicKey = async (currentPublicKey = {}, deviceId = '') => {
|
|
||||||
const result = await registerXeapiKey(
|
|
||||||
{
|
|
||||||
deviceId,
|
|
||||||
currentKeyVersion: currentPublicKey.version || '',
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
)
|
|
||||||
|
|
||||||
const publicKey = result.body
|
|
||||||
if (!publicKey.sk && currentPublicKey.sk) {
|
|
||||||
publicKey.sk = currentPublicKey.sk
|
|
||||||
}
|
|
||||||
if (!publicKey.sk) {
|
|
||||||
throw new Error('xeapi public key response missing sk')
|
|
||||||
}
|
|
||||||
return publicKey
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
getXeapiPublicKey,
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user