mirror of
https://github.com/NeteaseCloudMusicApiEnhanced/api-enhanced.git
synced 2026-10-20 22:51:23 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
19c7dad6db | ||
|
|
2db6684453 | ||
|
|
90de64140f | ||
|
|
2aab9957df | ||
|
|
373d5eedb3 | ||
|
|
c1cbdcb962 | ||
|
|
37b64ab01e | ||
|
|
95367213f7 | ||
|
|
bd407cee81 | ||
|
|
a91ea8f832 | ||
|
|
4a45a14aa7 | ||
|
|
acbea014be | ||
|
|
8ad30b591d | ||
|
|
cb449463fb | ||
|
|
91db12d0f5 | ||
|
|
135df9edda | ||
|
|
548f6c170a | ||
|
|
17c07960f9 | ||
|
|
c4d448641a | ||
|
|
77a9269828 | ||
|
|
9a16c89714 | ||
|
|
a8c781fd64 | ||
|
|
690b462577 | ||
|
|
25ac0c041f | ||
|
|
edef19c3fb | ||
|
|
d55d92cd00 | ||
|
|
d9253bf930 | ||
|
|
f5ce55bcb4 | ||
|
|
4480bd4359 | ||
|
|
e43b123c8a | ||
|
|
ed9bdd6ffa | ||
|
|
7b733e54ab | ||
|
|
edfaf6c0d1 | ||
|
|
f83ad951f6 | ||
|
|
22003c2c32 | ||
|
|
8376699f5e | ||
|
|
5c98a9a1ea | ||
|
|
7c17de7f09 | ||
|
|
a7e8d485f3 | ||
|
|
0957be6e76 | ||
|
|
4a91f77e62 | ||
|
|
a7437f7ca4 | ||
|
|
498945c401 | ||
|
|
7b17a08628 | ||
|
|
0b5c3927c9 | ||
|
|
881267c56d | ||
|
|
d7c0db9455 | ||
|
|
c1d14a86d9 | ||
|
|
a2a2a40d71 | ||
|
|
543660fcdf | ||
|
|
98a95002b2 | ||
|
|
c4a3d83abe | ||
|
|
7753b5de41 | ||
|
|
c1fe1eb925 | ||
|
|
3357317a1d | ||
|
|
a08f425ae7 | ||
|
|
5b780addba | ||
|
|
fc9f41839c | ||
|
|
5859944563 | ||
|
|
aaa6459e55 | ||
|
|
a898e1d86c | ||
|
|
4045f1ad3f | ||
|
|
e6214a3f73 | ||
|
|
686e8bf969 | ||
|
|
9214cb945f | ||
|
|
a79a2d9ef6 | ||
|
|
9608da9b96 | ||
|
|
93b3e624d8 | ||
|
|
15f515c1ba | ||
|
|
e255e8f334 | ||
|
|
16aa61f98e | ||
|
|
a939003b92 | ||
|
|
8f4873f2e2 | ||
|
|
a8b45a8b8b | ||
|
|
1a70281c76 | ||
|
|
f5f4ff6eda | ||
|
|
eb1b5ba0ea | ||
|
|
6732fc7c32 | ||
|
|
6ce6b84015 | ||
|
|
63d89aa906 | ||
|
|
8c212be86b | ||
|
|
89de56d2a2 | ||
|
|
d298025d7c | ||
|
|
a3287a0180 | ||
|
|
9adfd50c58 | ||
|
|
fda7c9e5b3 | ||
|
|
a8266c6d7b | ||
|
|
64a6b9677a | ||
|
|
20d3a1a7a5 | ||
|
|
4826c65042 | ||
|
|
7822ab08d3 | ||
|
|
41bd6d82ce | ||
|
|
6f8dd1d971 | ||
|
|
ddd0af5474 | ||
|
|
fbd2e06419 | ||
|
|
4442fb4341 | ||
|
|
35d1c61cb4 | ||
|
|
a183e153ef | ||
|
|
2fa78c3826 | ||
|
|
5ae1eab14c | ||
|
|
321c25bd7d | ||
|
|
f527c1ef73 | ||
|
|
0a5a1242f2 | ||
|
|
6946dc8e14 | ||
|
|
22b78779ca | ||
|
|
35607f5050 | ||
|
|
60af95ac06 | ||
|
|
46073e5f50 | ||
|
|
bf12abe2f1 | ||
|
|
1491b91829 | ||
|
|
63f669d7f8 | ||
|
|
d525204dda | ||
|
|
6eb31984fd | ||
|
|
9faeb0aa25 | ||
|
|
f6db3e76e9 | ||
|
|
9163d48f0a | ||
|
|
1454ee216c | ||
|
|
83ee2cda34 | ||
|
|
3c352b5752 | ||
|
|
60ee927e32 | ||
|
|
eb07a525eb | ||
|
|
6fc4231142 | ||
|
|
fa69736300 | ||
|
|
73a5e9d788 | ||
|
|
dc6f56e8a8 | ||
|
|
2c9da8507d | ||
|
|
1db354464f | ||
|
|
395a80e74b | ||
|
|
6550c7c827 | ||
|
|
c959866436 | ||
|
|
e3a1c041b6 | ||
|
|
e2324e26da | ||
|
|
f01e770c35 | ||
|
|
abb56ec014 | ||
|
|
0300c1b51e | ||
|
|
9896242333 | ||
|
|
e3812be97b | ||
|
|
e7ff0da4ba | ||
|
|
65261fa658 | ||
|
|
cb5b2e7d0f | ||
|
|
f58fd4b50d | ||
|
|
1a5f91a192 | ||
|
|
50cc26f297 | ||
|
|
c92613b19e | ||
|
|
7aacc8990b | ||
|
|
8e934789a1 | ||
|
|
d26e6b0615 | ||
|
|
a9c33de182 | ||
|
|
39e0e0718d | ||
|
|
6175d55f89 |
@@ -0,0 +1,5 @@
|
|||||||
|
# CodeGraph data files — local to each machine, not for committing.
|
||||||
|
# Ignore everything in .codegraph/ except this file itself, so transient
|
||||||
|
# files (the database, daemon.pid, sockets, logs) never show up in git.
|
||||||
|
*
|
||||||
|
!.gitignore
|
||||||
@@ -14,15 +14,16 @@ body:
|
|||||||
id: terms
|
id: terms
|
||||||
attributes:
|
attributes:
|
||||||
label: 确认事项
|
label: 确认事项
|
||||||
description: 在提交功能请求前,请确认以下事项
|
description: |
|
||||||
|
在提交功能请求前,请确认以下事项
|
||||||
|
|
||||||
|
- 我已经搜索了现有的issues,确认这不是重复请求
|
||||||
|
- 我已经查看了项目文档
|
||||||
|
- 该项目符合该项目的目标和范围
|
||||||
|
- 我确认我提供的接口请求是合理且有意义的
|
||||||
|
|
||||||
options:
|
options:
|
||||||
- label: 我已经搜索了现有的issues,确认这不是重复请求
|
- label: 我已确认以上事项
|
||||||
required: true
|
|
||||||
- label: 我已经查看了项目文档
|
|
||||||
required: true
|
|
||||||
- label: 该项目符合该项目的目标和范围
|
|
||||||
required: true
|
|
||||||
- label: 我确认我提供的接口请求是合理且有意义的
|
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -34,10 +34,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: '18'
|
node-version: '18'
|
||||||
|
|
||||||
|
- name: Setup pnpm
|
||||||
|
run: npm install -g pnpm@9
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: pnpm install --frozen-lockfile
|
||||||
npm install -g pnpm
|
|
||||||
pnpm install
|
|
||||||
|
|
||||||
- name: Build for ${{ matrix.platform }}
|
- name: Build for ${{ matrix.platform }}
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ci-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check:
|
||||||
|
name: Lint / Unit / Docs
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node-version: [18, 22, 24]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- uses: pnpm/action-setup@v6
|
||||||
|
with:
|
||||||
|
version: 9
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v6
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node-version }}
|
||||||
|
cache: pnpm
|
||||||
|
|
||||||
|
# frozen-lockfile: 顺带校验 package.json 与 pnpm-lock.yaml 是否一致
|
||||||
|
- name: Install dependencies (frozen lockfile)
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Lint
|
||||||
|
run: pnpm lint
|
||||||
|
|
||||||
|
# 纯单测,不需要网络,稳定必过
|
||||||
|
- name: Unit tests
|
||||||
|
run: pnpm exec mocha -r intelli-espower-loader -t 60000 main.test.js --exit
|
||||||
|
|
||||||
|
- name: Docs format check
|
||||||
|
run: pnpm docs:check
|
||||||
|
|
||||||
|
integration:
|
||||||
|
# server.test.js 会启动真实服务器并请求真实网易云 API,
|
||||||
|
# 可能因上游风控/限流偶发失败,因此不阻塞合并
|
||||||
|
name: Integration tests (real NetEase API)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
continue-on-error: true
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- uses: pnpm/action-setup@v6
|
||||||
|
with:
|
||||||
|
version: 9
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v6
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
cache: pnpm
|
||||||
|
|
||||||
|
- name: Install dependencies (frozen lockfile)
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Run full test suite
|
||||||
|
run: pnpm test
|
||||||
@@ -6,8 +6,6 @@ permissions:
|
|||||||
on:
|
on:
|
||||||
issues:
|
issues:
|
||||||
types: [opened, labeled]
|
types: [opened, labeled]
|
||||||
issue_comment:
|
|
||||||
types: [created]
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
welcome-new-issues:
|
welcome-new-issues:
|
||||||
@@ -19,44 +17,21 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
issue-number: ${{ github.event.issue.number }}
|
issue-number: ${{ github.event.issue.number }}
|
||||||
body: |
|
body: |
|
||||||
## 快速链接
|
感谢您在该仓库提出问题!我们非常重视您的反馈,并希望尽快为您提供帮助。
|
||||||
|
|
||||||
在我们处理您的问题之前,您可以先查看以下资源:
|
如果您在一周左右的时间里没有收到我们的回复,请在下方留言,以便我们注意到。我们的大部分审核都是志愿者,有时难免会有疏漏。
|
||||||
|
|
||||||
|
你可能会想要尝试以下方法来获得更快的帮助:
|
||||||
- 📖 查看[项目文档](https://neteasecloudmusicapienhanced.js.org)
|
- 📖 查看[项目文档](https://neteasecloudmusicapienhanced.js.org)
|
||||||
- 💬 加入[QQ交流群](https://qm.qq.com/q/TpeP9Uv2yk)
|
- 💬 加入[QQ交流群](https://qm.qq.com/q/TpeP9Uv2yk)
|
||||||
- 🔍 搜索[现有issues](https://github.com/neteasecloudmusicapienhanced/api-enhanced/issues) 看是否有类似问题
|
- 🔍 搜索[现有issues](https://github.com/neteasecloudmusicapienhanced/api-enhanced/issues) 看是否有类似问题
|
||||||
|
|
||||||
handle-help-wanted:
|
|
||||||
if: contains(github.event.issue.labels.*.name, 'question')
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Add help resources
|
|
||||||
uses: peter-evans/create-or-update-comment@v5
|
|
||||||
with:
|
|
||||||
issue-number: ${{ github.event.issue.number }}
|
|
||||||
body: |
|
|
||||||
🆘 需要帮助?
|
|
||||||
|
|
||||||
这里有一些可能对您有用的资源:
|
|
||||||
|
|
||||||
📚 **文档资源**
|
|
||||||
- [项目文档](https://neteasecloudmusicapienhanced.js.org)
|
|
||||||
|
|
||||||
🔍 **常见问题**
|
|
||||||
- 搜索[已关闭的issues](https://github.com/neteasecloudmusicapienhanced/api-enhanced/issues?q=is%3Aissue+is%3Aclosed) 看看是否有类似的问题已经被解答。
|
|
||||||
|
|
||||||
💬 **即时帮助**
|
|
||||||
- 加入QQ群:https://qm.qq.com/q/TpeP9Uv2yk
|
|
||||||
|
|
||||||
如果以上资源无法解决您的问题,请提供更多详细信息,我们会尽快为您解答!
|
|
||||||
|
|
||||||
stale-issues:
|
stale-issues:
|
||||||
if: github.event.action == 'opened'
|
if: github.event.action == 'opened'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Mark stale issues
|
- name: Mark stale issues
|
||||||
uses: actions/stale@v10
|
uses: actions/stale@v11
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
stale-issue-message: |
|
stale-issue-message: |
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
name: opencode
|
||||||
|
|
||||||
|
on:
|
||||||
|
issue_comment:
|
||||||
|
types: [created]
|
||||||
|
pull_request_review_comment:
|
||||||
|
types: [created]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
issues: write
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
bot:
|
||||||
|
if: |
|
||||||
|
(contains(github.event.comment.body, ' /oc') ||
|
||||||
|
startsWith(github.event.comment.body, '/oc') ||
|
||||||
|
contains(github.event.comment.body, ' /opencode') ||
|
||||||
|
startsWith(github.event.comment.body, '/opencode')) &&
|
||||||
|
github.actor != 'opencode-agent[bot]' &&
|
||||||
|
github.actor != 'yukka-agent[bot]' &&
|
||||||
|
github.actor != 'github-actions[bot]'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
steps:
|
||||||
|
- name: Fetch config from CF Worker
|
||||||
|
id: cfg
|
||||||
|
uses: MoeFurina/pull-oc-config@v1
|
||||||
|
with:
|
||||||
|
url: https://bot-storage.interface.focalors.ltd/config
|
||||||
|
|
||||||
|
- name: Generate bot token
|
||||||
|
id: token
|
||||||
|
uses: actions/create-github-app-token@v3
|
||||||
|
with:
|
||||||
|
client-id: ${{ steps.cfg.outputs.app_id }}
|
||||||
|
private-key: ${{ steps.cfg.outputs.app_private_key }}
|
||||||
|
owner: ${{ github.repository_owner }}
|
||||||
|
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
# 需要持久化凭证: opencode action 内部会 git fetch origin <分支> 拉取 PR 代码
|
||||||
|
persist-credentials: true
|
||||||
|
token: ${{ steps.token.outputs.token }}
|
||||||
|
|
||||||
|
- name: Configure git identity
|
||||||
|
run: |
|
||||||
|
git config --global user.name "yukka-agent[bot]"
|
||||||
|
git config --global user.email "yukka-agent[bot]@users.noreply.github.com"
|
||||||
|
|
||||||
|
- name: Run opencode
|
||||||
|
uses: anomalyco/opencode/github@latest
|
||||||
|
env:
|
||||||
|
OPENCODE_API_KEY: ${{ steps.cfg.outputs.opencode_api_key }}
|
||||||
|
GITHUB_TOKEN: ${{ steps.token.outputs.token }}
|
||||||
|
OPENCODE_PERMISSION: '{"permission":{"*":"allow","question":"deny"}}'
|
||||||
|
with:
|
||||||
|
model: opencode/muse-spark-1.3-contributor-free
|
||||||
|
use_github_token: true
|
||||||
|
prompt: |
|
||||||
|
${{ steps.cfg.outputs.opencode_prompt }}
|
||||||
|
|
||||||
|
## 用户指令(来自触发评论,请严格按此执行)
|
||||||
|
${{ github.event.comment.body }}
|
||||||
|
|
||||||
|
## 用户要求Review时, 严格执行下面指令
|
||||||
|
${{ steps.cfg.outputs.pr_review_prompt }}
|
||||||
@@ -136,7 +136,7 @@ jobs:
|
|||||||
esac
|
esac
|
||||||
|
|
||||||
- name: Upload build artifact
|
- name: Upload build artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: ${{ matrix.platform }}-binary
|
name: ${{ matrix.platform }}-binary
|
||||||
path: release-artifacts/*
|
path: release-artifacts/*
|
||||||
@@ -165,17 +165,16 @@ jobs:
|
|||||||
fetch-tags: true
|
fetch-tags: true
|
||||||
|
|
||||||
- name: Download build artifacts
|
- name: Download build artifacts
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
path: release-artifacts
|
path: release-artifacts
|
||||||
|
merge-multiple: true
|
||||||
|
|
||||||
- name: Flatten artifacts
|
- name: List flattened 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
|
||||||
@@ -261,13 +260,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@v3
|
uses: docker/login-action@v4
|
||||||
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@v3
|
uses: docker/login-action@v4
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
|
|||||||
@@ -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/[email protected].2
|
uses: aormsby/[email protected].3
|
||||||
with:
|
with:
|
||||||
upstream_sync_repo: NeteaseCloudMusicApiEnhanced/api-enhanced
|
upstream_sync_repo: NeteaseCloudMusicApiEnhanced/api-enhanced
|
||||||
upstream_sync_branch: main
|
upstream_sync_branch: main
|
||||||
|
|||||||
@@ -10,3 +10,4 @@ anonymous_token
|
|||||||
.vercel
|
.vercel
|
||||||
.env
|
.env
|
||||||
IFLOW.md
|
IFLOW.md
|
||||||
|
precompiled
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# NeteaseCloudMusicApiEnhanced Agent 说明
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
- **包管理器**:开发与 CI 使用 `pnpm`,`Dockerfile` 也用 pnpm(`pnpm@9 --frozen-lockfile`,匹配 `pnpm-lock.yaml`)。仓库没有 `yarn.lock`,不要引入 yarn。
|
||||||
|
- **Node 版本**:README 推荐 Node 22+;`package.json` 的 `engines` 声明 `>=12`;CI/打包在 Node 18–24 上运行。现代 Node 均可。
|
||||||
|
- **环境变量**:`server.js` 调用了 `dotenv.config()`,本地 `.env` 会被自动加载;所有支持的变量见 `.env.prod.example`。
|
||||||
|
|
||||||
|
## 常用命令
|
||||||
|
- 安装依赖:`pnpm i`
|
||||||
|
- 启动服务:`pnpm start`(等价 `node app.js`);热重载开发:`pnpm dev`(nodemon)
|
||||||
|
- 跑测试:`pnpm test`(Mocha,超时 60s)
|
||||||
|
- Lint:`pnpm lint`;自动修复:`pnpm lint-fix`
|
||||||
|
- 文档格式化检查/修复:`pnpm docs:check` / `pnpm docs:format`
|
||||||
|
- 打包独立二进制:`pnpm pkgwin` / `pkglinux` / `pkgmacos`
|
||||||
|
|
||||||
|
## 目录内非路由文件
|
||||||
|
- `plugins/`——内部上传辅助(`upload.js`、`songUpload.js`),被 `module/*` require,本身不是路由。
|
||||||
|
- `module_example/`——接口模板与 `main.js` 库调用示例(含 `test.js` 演示 `login_cellphone`→`song_url` 链路),不是运行时代码。
|
||||||
|
- `examples/get_static_moddef.js`——用 `getModulesDefinitions(..., false)`(`doRequire=false`)把全部路由导出为 `moddef.json`。改路由注册逻辑时可跑它核对。
|
||||||
|
- `module_types/` 为空目录;类型声明在根目录 `interface.d.ts`(`types` 字段)。
|
||||||
|
|
||||||
|
## 架构
|
||||||
|
- `app.js`(也是 `bin`)——服务入口。先确保 `os.tmpdir()` 里存在 `anonymous_token`,执行 `generateConfig()` 刷新匿名 cookie 与 xeapi 公钥,再调用 `server.serveNcmApi()`。
|
||||||
|
- `server.js`——Express 工厂。`constructServer()` 自动扫描 `module/*.js`,每个文件注册一条路由(文件名 `_` 转 `/`,如 `album_new.js` → `/album/new`;特例 `daily_signin`/`fm_trash`/`personal_fm` 硬编码在 `server.js` 的 `special` 对象里)。`serveNcmApi()` 监听 `PORT`(默认 3000)/`HOST`。
|
||||||
|
- `main.js`——作为依赖被引入时的入口(`main` 字段)。把每个 `module/*` 导出为同名函数 `name(data)`,另导出 `server`、`serveNcmApi`、`getModulesDefinitions`。
|
||||||
|
- `module/*.js`——每个接口一个文件,标准写法:`module.exports = (query, request) => request(path, data, createOption(query))`。`createOption` 在 `util/option.js`,负责 crypto、cookie(回退到 `NETEASE_COOKIE`)、proxy、realIP/randomCNIP、headers、timeout。
|
||||||
|
- `util/request.js`——唯一的对外 HTTP 层(axios)。按 `crypto`(`api`/`eapi`/`weapi`/`linuxapi`/`xeapi`)加密并设置 IP 头;在 require 时同步读取 `os.tmpdir()` 里的 `anonymous_token` 与 `xeapi_public_key`。
|
||||||
|
- `util/config.json`——运行时配置:网易域名 + `APP_CONF.encrypt: true`(默认走 eapi 加密)。已被 git 跟踪,改动会改变全局默认行为。
|
||||||
|
- `index.js` / `index.mjs`——`require('./app.js')` 的薄包装,供 Vercel(`vercel.json`)和 ESM 导入使用。
|
||||||
|
- `server.js` 里有一段非显而易见的逻辑:环境变量 `ENABLE_GENERAL_UNBLOCK=true` 时,`/song/url/v1` 的响应会被自动解灰(走 `@neteasecloudmusicapienhanced/unblockmusic-utils`);另外 `query.noCookie` 为真时不向响应写 `Set-Cookie`。
|
||||||
|
|
||||||
|
## 新增/修改接口
|
||||||
|
- 新建 `module/xxx.js` 会自动挂载路由,无需注册;**文件名即路由**。
|
||||||
|
- 照抄同目录模块的写法(选对 `crypto`),用 `createOption(query)` 生成请求选项。
|
||||||
|
- 改文件名/路由会破坏已有客户端,尽量保持旧路径兼容。
|
||||||
|
|
||||||
|
## 测试
|
||||||
|
- `pnpm test` 跑 `server.test.js` + `main.test.js`。`server.test.js` 在 `before()` 里启动真实服务器,`test/*.test.js` 全部请求**真实网易云 API**——必须联网,且可能因上游风控/限流偶发失败。`main.test.js` 是纯单测,不需要网络。
|
||||||
|
- **CI 门禁只看稳定项**:`ci-check.yml` 在 Node 18/22/24 上跑 `pnpm lint` + `main.test.js` + `pnpm docs:check`;完整集成测试(`server.test.js`)是 `continue-on-error`,不阻塞合并。本地跑全量测试失败先怀疑上游风控。
|
||||||
|
- 测试使用 `power-assert`(经 `intelli-espower-loader`),普通 `assert` 写法也会输出详细 diff。
|
||||||
|
- 只跑单个用例:`pnpm exec mocha -r intelli-espower-loader -t 60000 --grep "<describe/it 名字>" server.test.js main.test.js --exit`
|
||||||
|
- 只跑离线单测:`pnpm exec mocha -r intelli-espower-loader -t 60000 main.test.js --exit`
|
||||||
|
|
||||||
|
## 坑与注意
|
||||||
|
- **改 `package.json` 的 `version` 会触发自动发布**:`release-on-version-change.yml` 按 paths 过滤——只要 push 到 `main` 时 `package.json` 有改动且 version 变了,就自动打 tag + GitHub Release(`pkg` 三平台二进制)、推送 Docker 镜像(Docker Hub + GHCR 多架构)、`pnpm publish` 到 npm;tag 已存在则跳过。别顺手改版本号,也别往 `package.json` 里写无关改动凑提交。
|
||||||
|
- **没有实际 git hooks**:`package.json` 里配了 `lint-staged` 和 husky,但 `.husky/` 下只有 `_` 脚手架目录、没有真正的 hook 文件,commit 时不会自动跑任何检查,自己记得 `pnpm lint-fix`。
|
||||||
|
- **代理环境变量已失效**:README 里关于 `http_proxy`/`https_proxy` 的警告来自旧 `request` 库时代;现在 `util/request.js` 用 axios + 自定义 keep-alive agent,且显式 `proxy: false`,环境变量代理不会生效。按请求走 `query.proxy` 参数(支持 PAC 和 http 隧道)。
|
||||||
|
- **启动令牌在系统临时目录**:`anonymous_token`、`xeapi_public_key` 存放在 `os.tmpdir()`,`util/request.js` 在 require 时同步读取。文件过期或被清空就重启服务(或调用 `generateConfig()`);首次启动先写空文件再刷新。
|
||||||
|
- **`docs:check` 校验的是 `public/docs/home.md`(docsify 站点)**,不是 README;脚本默认只处理该文件,换行会被统一为 `\n`、连续空行压缩到 2 行,PR 改动它后记得跑 `pnpm docs:format`。
|
||||||
|
- **ESLint 9 flat config**:`eslint.config.js`,风格由 `eslint-plugin-prettier` 强制(2 空格缩进、单引号、分号、`endOfLine: auto`),并 `globalIgnores(['**/public/'])`——`public/` 下的前端页面不参与 lint。
|
||||||
|
- **`pnpm-workspace.yaml` 是单包工作区**(`packages: ['.']`),并带 `allowBuilds`(core-js/es5-ext)与 `minimumReleaseAgeExclude` 白名单;改依赖安装行为时先看它。
|
||||||
-1418
File diff suppressed because it is too large
Load Diff
+7
-2
@@ -2,14 +2,19 @@ FROM node:lts-alpine
|
|||||||
|
|
||||||
RUN apk add --no-cache tini
|
RUN apk add --no-cache tini
|
||||||
|
|
||||||
ENV NODE_ENV production
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
|
RUN npm install -g pnpm@9
|
||||||
|
|
||||||
USER node
|
USER node
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY --chown=node:node . ./
|
COPY --chown=node:node . ./
|
||||||
|
|
||||||
RUN yarn --network-timeout=100000
|
# --prod 模式下 husky 不会被安装,prepare 脚本会因找不到 husky 而失败,
|
||||||
|
# 故显式跳过生命周期脚本;本项目生产运行也不依赖任何 postinstall 步骤。
|
||||||
|
RUN pnpm install --frozen-lockfile --prod --ignore-scripts
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
|
|
||||||
## 项目简介
|
## 项目简介
|
||||||
|
|
||||||
网易云音乐第三方 Node.js API, 支持丰富的音乐相关接口,适合自建服务、二次开发和多平台部署(如果原版诈尸, 我会及时同步 or 归档)。
|
网易云音乐第三方 Node.js API, 支持丰富的音乐相关接口,适合自建服务、二次开发和多平台部署
|
||||||
|
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
>
|
>
|
||||||
@@ -45,14 +45,14 @@
|
|||||||
|
|
||||||
### 环境要求
|
### 环境要求
|
||||||
|
|
||||||
- Node.js 18 及以上
|
- Node.js 22 及以上
|
||||||
- 推荐使用 pnpm 进行依赖管理
|
- 推荐使用 pnpm 进行依赖管理
|
||||||
|
|
||||||
### 安装
|
### 安装
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/neteasecloudmusicapienhanced/api-enhanced.git
|
git clone https://github.com/neteasecloudmusicapienhanced/api-enhanced.git
|
||||||
cd api
|
cd api-enhanced
|
||||||
pnpm i
|
pnpm i
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -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等加密算法所做的贡献
|
感谢大佬们为逆向eapi, weapi, xeapi等加密算法所做的贡献
|
||||||
|
|
||||||
项目参考:
|
项目参考:
|
||||||
|
|
||||||
@@ -225,6 +225,8 @@ 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 生态
|
||||||
|
|
||||||
| 语言 | 作者 | 地址 | 类型 |
|
| 语言 | 作者 | 地址 | 类型 |
|
||||||
@@ -244,6 +246,8 @@ 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)
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ 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 { getXeapiPublicKey } = require('./util/xeapiKey')
|
||||||
|
const { loadNeapiKey } = require('./util/neapiKey')
|
||||||
|
const registerNeapikey = require('./module/register_neapikey')
|
||||||
const tmpPath = require('os').tmpdir()
|
const tmpPath = require('os').tmpdir()
|
||||||
|
|
||||||
async function generateConfig() {
|
async function generateConfig() {
|
||||||
@@ -37,5 +39,17 @@ async function generateConfig() {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
console.log(error)
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
const current = loadNeapiKey()
|
||||||
|
await registerNeapikey(
|
||||||
|
{
|
||||||
|
version: current ? current.version : 0,
|
||||||
|
behavior: current ? 'update' : 'open',
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
module.exports = generateConfig
|
module.exports = generateConfig
|
||||||
|
|||||||
Vendored
+1074
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,51 @@
|
|||||||
|
// 获取广告
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
|
||||||
|
module.exports = async (query, request) => {
|
||||||
|
const data = {
|
||||||
|
type_ids: query.type_ids || '["400002_0"]',
|
||||||
|
}
|
||||||
|
|
||||||
|
const option = createOption(query, 'xeapi', 'v3')
|
||||||
|
|
||||||
|
const res = await request(`/api/ad/get`, data, option)
|
||||||
|
const raw = res.body
|
||||||
|
|
||||||
|
// 提取广告中的 req_id
|
||||||
|
let reqId = ''
|
||||||
|
try {
|
||||||
|
if (raw?.ads) {
|
||||||
|
const ad = Object.values(raw.ads)[0]
|
||||||
|
// 逆向 v9.5.61:客户端从 ad.adExtMap["req_id"] 取 reqUid
|
||||||
|
if (ad?.adExtMap) {
|
||||||
|
if (typeof ad.adExtMap === 'string') {
|
||||||
|
try {
|
||||||
|
reqId = JSON.parse(ad.adExtMap).req_id || ''
|
||||||
|
} catch (_) {}
|
||||||
|
} else {
|
||||||
|
reqId = ad.adExtMap.req_id || ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 兜底:adLogId.requestId / ad.reqId / extJson.contextInfo.req_id
|
||||||
|
if (!reqId && ad?.adLogId?.requestId) reqId = ad.adLogId.requestId
|
||||||
|
if (!reqId && ad?.reqId) reqId = ad.reqId
|
||||||
|
if (!reqId && ad?.extJson) {
|
||||||
|
try {
|
||||||
|
const ext = JSON.parse(ad.extJson)
|
||||||
|
reqId = ext?.contextInfo?.req_id || ''
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
code: 200,
|
||||||
|
ads: raw?.ads || null,
|
||||||
|
message: raw?.message || null,
|
||||||
|
extra: { reqId },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
// 获取免费听时长状态
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
entrance: 'FREE_LISTEN_RN',
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
`/api/ad/homepage/free/tab/extend/v2`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'xeapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,252 @@
|
|||||||
|
// 看广告免费听歌 - 领取免费听权益
|
||||||
|
// 请求流程(基于逆向网易云音乐 v9.5.61 原生 Kotlin 源码,classes5/18/19.dex):
|
||||||
|
// 1. 从广告平台拉广告 → 用户看完/点击广告 → 获取 ad 对象的 extJson.contextInfo.req_id 作为 reqUid
|
||||||
|
// 2. 调用本接口传入 reqUid 及相关权益参数,领取权益
|
||||||
|
// 3. 服务器返回 gainFlag 等标识,用于展示领取结果
|
||||||
|
//
|
||||||
|
// 调用链(逆向还原):
|
||||||
|
// AdDSLIncentiveVideoRightsHelper.requestRightsGainInner
|
||||||
|
// → AdDSLUtils.requestRightGain(ad, exposeTime, clickTime, creativeType, cb)
|
||||||
|
// → 构造 ListeningRightRequestParams(21 字段)→ JSON.stringify
|
||||||
|
// → body = { "reqParam": "..." } → POST /api/ad/listening/rights/gain
|
||||||
|
// (注意:客户端 AdDSLIncentiveVideoRightsHelper 写死 creativeType=36)
|
||||||
|
//
|
||||||
|
// 接口不只领取听歌时长,还包含"看视频得云贝"等权益:
|
||||||
|
// rightsGainMethod=6 (LAXIN_EXPOSE_OR_DOWNLOAD 拉新曝光/下载分段权益) 时,
|
||||||
|
// 弹窗 AdLaxinSegmentedGuideDialog 展示 "获得X云贝"(2000 云贝等),
|
||||||
|
// 权益数值由广告下发的 AdLaxinSegmentedRightsGuidePopup.rightsValue 决定,
|
||||||
|
// extraRightsType 作为权益类型随本接口上传。
|
||||||
|
//
|
||||||
|
// rightsGainMethod 枚举(h80/a):
|
||||||
|
// 1=EXPOSE 曝光, 2=EXPOSE_CLICK 曝光+点击, 3=EXPOSE_DOWNLOAD 曝光+下载,
|
||||||
|
// 4=CLICK_STAY 点击+停留, 5=EXPOSURE_OR_CLICK_STAY 曝光或点击停留,
|
||||||
|
// 6=LAXIN_EXPOSE_OR_DOWNLOAD 拉新曝光/下载分段权益
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
const adGet = require('./ad_get.js')
|
||||||
|
|
||||||
|
// 安全 JSON 解析(字符串字段可能是 JSON 文本)
|
||||||
|
function safeParse(str, fallback) {
|
||||||
|
if (typeof str !== 'string') return fallback
|
||||||
|
try {
|
||||||
|
return JSON.parse(str)
|
||||||
|
} catch (_) {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = async (query, request) => {
|
||||||
|
const time = Date.now()
|
||||||
|
|
||||||
|
// 从广告对象自动补齐请求字段。
|
||||||
|
// 实测 v9.5.61 真实 API 返回(/ad/get)确认的字段路径:
|
||||||
|
// reqUid = ad.extJson.contextInfo.req_id(真实响应中唯一存在)
|
||||||
|
// contextInfo = ad.extJson.contextInfo(完整对象序列化)
|
||||||
|
// generalRightsInfo = ad.generalRightsInfo(字符串 JSON)
|
||||||
|
// creativeType = ad.creativeType
|
||||||
|
// rightsGainMethod / extraRightsType / rightsGainDuration / rightsGainType /
|
||||||
|
// extraRightsGainMethod / extraRightsGainDuration / nextRightsGainDuration /
|
||||||
|
// rightsExtJson / source / rightsUpperLimit / qualified
|
||||||
|
// = ad.generalRightsInfo(字符串)解析后取值
|
||||||
|
// (逆向类字段 adExtMap/adLogId/listeningRightHintInfo 真实响应中不存在,仅兜底)
|
||||||
|
// sniffTime = method==3 或 6 时 currentTimeMillis
|
||||||
|
let reqUid = query.reqUid || ''
|
||||||
|
let contextInfo = query.contextInfo
|
||||||
|
let creativeType = query.creativeType
|
||||||
|
let generalRightsInfo = query.generalRightsInfo
|
||||||
|
|
||||||
|
// 广告 hint 配置:仅当调用方未显式传参时用广告下发值兜底
|
||||||
|
const hint = {}
|
||||||
|
|
||||||
|
if (!reqUid || contextInfo === undefined || creativeType === undefined) {
|
||||||
|
try {
|
||||||
|
const adRes = await adGet(
|
||||||
|
{ ...query, type_ids: query.type_ids || '["400002_0"]' },
|
||||||
|
request,
|
||||||
|
)
|
||||||
|
const ad = Object.values(adRes?.body?.ads || {})[0]
|
||||||
|
if (!ad) throw new Error('ads 为空(未登录或广告位无广告)')
|
||||||
|
|
||||||
|
// reqUid:真实 API 返回中 req_id 在 ad.extJson.contextInfo.req_id(实测 v9.5.61)
|
||||||
|
// (逆向类字段 adExtMap/adLogId 在真实响应中不存在,仅作兜底)
|
||||||
|
if (!reqUid) {
|
||||||
|
const ext =
|
||||||
|
typeof ad?.extJson === 'string'
|
||||||
|
? safeParse(ad.extJson, {})
|
||||||
|
: ad?.extJson || {}
|
||||||
|
const extMap =
|
||||||
|
typeof ad?.adExtMap === 'string'
|
||||||
|
? safeParse(ad.adExtMap, {})
|
||||||
|
: ad?.adExtMap
|
||||||
|
reqUid =
|
||||||
|
ext?.contextInfo?.req_id ||
|
||||||
|
extMap?.req_id ||
|
||||||
|
ad?.adLogId?.requestId ||
|
||||||
|
ad?.reqId ||
|
||||||
|
adRes?.body?.extra?.reqId ||
|
||||||
|
''
|
||||||
|
}
|
||||||
|
// contextInfo:真实 API 返回中在 ad.extJson.contextInfo(实测 v9.5.61)
|
||||||
|
if (contextInfo === undefined) {
|
||||||
|
const ext =
|
||||||
|
typeof ad?.extJson === 'string'
|
||||||
|
? safeParse(ad.extJson, {})
|
||||||
|
: ad?.extJson || {}
|
||||||
|
const ci =
|
||||||
|
ext?.contextInfo || ad?.adLogId?.contextInfo || ad?.showContext
|
||||||
|
if (ci) {
|
||||||
|
contextInfo = typeof ci === 'string' ? ci : JSON.stringify(ci)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (creativeType === undefined && ad?.creativeType !== undefined) {
|
||||||
|
creativeType = ad.creativeType
|
||||||
|
}
|
||||||
|
if (generalRightsInfo === undefined && ad?.generalRightsInfo) {
|
||||||
|
generalRightsInfo =
|
||||||
|
typeof ad.generalRightsInfo === 'string'
|
||||||
|
? ad.generalRightsInfo
|
||||||
|
: JSON.stringify(ad.generalRightsInfo)
|
||||||
|
}
|
||||||
|
// 缓存广告下发的领取配置(后续用于兜底)
|
||||||
|
// 实测 v9.5.61:rightsGainMethod/rightsUpperLimit/qualified 等在
|
||||||
|
// ad.generalRightsInfo(字符串)里,ad.listeningRightHintInfo 真实响应中不存在
|
||||||
|
const hintCfg =
|
||||||
|
typeof ad?.listeningRightHintInfo === 'string'
|
||||||
|
? safeParse(ad.listeningRightHintInfo, {})
|
||||||
|
: ad?.listeningRightHintInfo || {}
|
||||||
|
const gri =
|
||||||
|
typeof ad?.generalRightsInfo === 'string'
|
||||||
|
? safeParse(ad.generalRightsInfo, {})
|
||||||
|
: ad?.generalRightsInfo || {}
|
||||||
|
Object.assign(hint, hintCfg, gri)
|
||||||
|
console.log(`自动获取 reqUid: ${reqUid}`)
|
||||||
|
} catch (e) {
|
||||||
|
// 获取广告失败,后续请求会因缺少 reqUid 被拒绝
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const rightsGainMethod = query.rightsGainMethod
|
||||||
|
? parseInt(query.rightsGainMethod)
|
||||||
|
: hint.rightsGainMethod || 2
|
||||||
|
|
||||||
|
const rightsParam = {
|
||||||
|
// 必填: 广告请求 ID,自动从 ad_get 获取
|
||||||
|
reqUid,
|
||||||
|
|
||||||
|
// 曝光时间戳
|
||||||
|
exposureTime: query.exposureTime ? parseInt(query.exposureTime) : time,
|
||||||
|
|
||||||
|
// 当前登录用户 ID(原版客户端从 Profile.getUserId() 获取)
|
||||||
|
userId: query.uid ? parseInt(query.uid) : undefined,
|
||||||
|
|
||||||
|
// 点击时间戳
|
||||||
|
clickTime: query.clickTime ? parseInt(query.clickTime) : time,
|
||||||
|
|
||||||
|
// 额外权益类型(拉新分段权益等,决定发放云贝/时长/下载,来自广告配置)
|
||||||
|
extraRightsType: query.extraRightsType
|
||||||
|
? parseInt(query.extraRightsType)
|
||||||
|
: hint.extraRightsType !== undefined
|
||||||
|
? parseInt(hint.extraRightsType)
|
||||||
|
: undefined,
|
||||||
|
|
||||||
|
// 是否连续播放(默认 false)
|
||||||
|
playContinuously: query.playContinuously ? true : false,
|
||||||
|
|
||||||
|
// 来源标识(原版从 ad.listeningRightHintInfo.source 读取)
|
||||||
|
source: query.source
|
||||||
|
? parseInt(query.source)
|
||||||
|
: hint.source !== undefined
|
||||||
|
? parseInt(hint.source)
|
||||||
|
: undefined,
|
||||||
|
|
||||||
|
// 广告创意类型(激励视频场景=36,优先取 query / 广告对象)
|
||||||
|
creativeType: creativeType !== undefined ? parseInt(creativeType) : 36,
|
||||||
|
|
||||||
|
// 权益领取方式(1~6 枚举,见文件头注释)
|
||||||
|
rightsGainMethod,
|
||||||
|
|
||||||
|
// 权益扩展方式与时长
|
||||||
|
extraRightsGainMethod: query.extraRightsGainMethod
|
||||||
|
? parseInt(query.extraRightsGainMethod)
|
||||||
|
: hint.extraRightsGainMethod !== undefined
|
||||||
|
? parseInt(hint.extraRightsGainMethod)
|
||||||
|
: undefined,
|
||||||
|
extraRightsGainDuration: query.extraRightsGainDuration
|
||||||
|
? parseInt(query.extraRightsGainDuration)
|
||||||
|
: hint.extraRightsGainDuration !== undefined
|
||||||
|
? parseInt(hint.extraRightsGainDuration)
|
||||||
|
: undefined,
|
||||||
|
nextRightsGainDuration: query.nextRightsGainDuration
|
||||||
|
? parseInt(query.nextRightsGainDuration)
|
||||||
|
: hint.nextRightsGainDuration !== undefined
|
||||||
|
? parseInt(hint.nextRightsGainDuration)
|
||||||
|
: undefined,
|
||||||
|
|
||||||
|
// 权益类型(含 RIGHTS_GAIN_TYPE_CURRENT_DAY 等取值)
|
||||||
|
rightsGainType: query.rightsGainType
|
||||||
|
? parseInt(query.rightsGainType)
|
||||||
|
: hint.rightsGainType !== undefined
|
||||||
|
? parseInt(hint.rightsGainType)
|
||||||
|
: undefined,
|
||||||
|
|
||||||
|
// 权益时长
|
||||||
|
rightsGainDuration: query.rightsGainDuration
|
||||||
|
? parseInt(query.rightsGainDuration)
|
||||||
|
: hint.rightsGainDuration !== undefined
|
||||||
|
? parseInt(hint.rightsGainDuration)
|
||||||
|
: undefined,
|
||||||
|
|
||||||
|
// 领取步骤(UNGAIN/GAINING/GAIN_FINISHED 对应的值)
|
||||||
|
gainMethodStep: query.gainMethodStep
|
||||||
|
? parseInt(query.gainMethodStep)
|
||||||
|
: undefined,
|
||||||
|
|
||||||
|
// 通用权益信息(ad.generalRightsInfo 序列化 JSON)
|
||||||
|
generalRightsInfo,
|
||||||
|
|
||||||
|
// 权益扩展信息
|
||||||
|
rightsExtJson: query.rightsExtJson || hint.rightsExtJson || undefined,
|
||||||
|
|
||||||
|
// 应用信息(下载类广告)
|
||||||
|
appInfo: query.appInfo ? JSON.parse(query.appInfo) : undefined,
|
||||||
|
|
||||||
|
// 广告上下文(ad.adLogId.contextInfo,客户端真实来源)
|
||||||
|
contextInfo,
|
||||||
|
|
||||||
|
// 应用是否已安装(下载类广告)
|
||||||
|
installed: query.installed ? parseInt(query.installed) : undefined,
|
||||||
|
|
||||||
|
// 嗅探时间:逆向确认(classes5.dex AdDSLUtils.requestRightGain):
|
||||||
|
// rightsGainMethod==3(曝光+下载) 或 6(LAXIN) 时传 currentTimeMillis
|
||||||
|
sniffTime:
|
||||||
|
rightsGainMethod === 3 || rightsGainMethod === 6
|
||||||
|
? query.sniffTime
|
||||||
|
? parseInt(query.sniffTime)
|
||||||
|
: time
|
||||||
|
: undefined,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理 undefined 字段
|
||||||
|
Object.keys(rightsParam).forEach((key) => {
|
||||||
|
if (rightsParam[key] === undefined) delete rightsParam[key]
|
||||||
|
})
|
||||||
|
|
||||||
|
// 将参数序列化为 reqParam 字符串 (与原生源码一致)
|
||||||
|
const data = {
|
||||||
|
reqParam: JSON.stringify(rightsParam),
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await request(
|
||||||
|
`/api/ad/listening/rights/gain`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'xeapi', 'v3'),
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
code: 200,
|
||||||
|
data: res.body,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
// 获取关注歌手的新歌曲和 MV
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
startTimestamp: query.startTimestamp || query.before || Date.now(),
|
||||||
|
sourceType: query.sourceType || 1,
|
||||||
|
limit: query.limit || 10,
|
||||||
|
firstRequest: query.firstRequest ?? true,
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
`/api/sub/artist/new/works/song-mv/list/v2`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'eapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
// 获取所有关注歌手最近的 50 首新歌
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
return request(
|
||||||
|
`/api/sub/artist/new/works/song/playall`,
|
||||||
|
{},
|
||||||
|
createOption(query, 'eapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
// 发送安全验证码
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
ctcode: query.ctcode || '86',
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
`/api/sms/captcha/safe/sent`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'eapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
// 发送验证码
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
ctcode: query.ctcode || '86',
|
||||||
|
secrete: 'music_middleuser_pclogin',
|
||||||
|
cellphone: query.phone,
|
||||||
|
scene: '0',
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
`/api/middle/captcha/sent/v1`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'eapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -6,5 +6,9 @@ module.exports = (query, request) => {
|
|||||||
cellphone: query.phone,
|
cellphone: query.phone,
|
||||||
countrycode: query.countrycode,
|
countrycode: query.countrycode,
|
||||||
}
|
}
|
||||||
return request(`/api/cellphone/existence/check`, data, createOption(query))
|
return request(
|
||||||
|
`/api/cellphone/existence/check`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'eapi'),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -25,6 +25,6 @@ module.exports = (query, request) => {
|
|||||||
return request(
|
return request(
|
||||||
`/api/resource/comments/${query.t}`,
|
`/api/resource/comments/${query.t}`,
|
||||||
data,
|
data,
|
||||||
createOption(query, 'weapi'),
|
createOption(query, 'eapi', 'v2'),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
const { resourceTypeMap } = require('../util/config.json')
|
||||||
|
// 发送评论
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
threadId: resourceTypeMap[query.type] + query.id,
|
||||||
|
content: query.content,
|
||||||
|
resourceType: '0',
|
||||||
|
expressionPicId: '-1',
|
||||||
|
bubbleId: '-1',
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
`/api/resource/comments/add`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'xeapi', 'v3'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
const { resourceTypeMap } = require('../util/config.json')
|
||||||
|
// 删除评论
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
commentId: query.cid,
|
||||||
|
threadId: resourceTypeMap[query.type] + query.id,
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
`/api/resource/comments/delete`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'xeapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
const { resourceTypeMap } = require('../util/config.json')
|
||||||
|
// 发送评论
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
threadId: resourceTypeMap[query.type] + query.id,
|
||||||
|
commentId: query.cid,
|
||||||
|
content: query.content,
|
||||||
|
resourceType: '0',
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
`/api/v1/resource/comments/reply`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'xeapi', 'v3'),
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
-1
@@ -48,7 +48,7 @@ module.exports = async (query, request) => {
|
|||||||
case 'linuxapi': {
|
case 'linuxapi': {
|
||||||
if (isReq) {
|
if (isReq) {
|
||||||
const pureHex = data.replace(/\s/g, '')
|
const pureHex = data.replace(/\s/g, '')
|
||||||
const decrypted = aesDecrypt(pureHex, linuxapiKey, '', 'hex')
|
const decrypted = aesDecrypt(pureHex, 'ecb', linuxapiKey, '', 'hex')
|
||||||
result = JSON.parse(decrypted.toString(CryptoJS.enc.Utf8))
|
result = JSON.parse(decrypted.toString(CryptoJS.enc.Utf8))
|
||||||
} else {
|
} else {
|
||||||
result = typeof data === 'string' ? JSON.parse(data) : data
|
result = typeof data === 'string' ? JSON.parse(data) : data
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
// 强制下线设备
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
key: query.deviceKey,
|
||||||
|
captcha: query.captcha || '',
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
`/api/middle/user/security/device/kickoff`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'eapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
// 登录设备列表
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
excStatus: '9',
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
`/api/middle/user/device/list`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'eapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
// 上报设备中心设备名称
|
||||||
|
// 官方 PC 端在登录成功后调用此接口,将当前登录设备在“登录设备管理”中的名称设置为自定义名称
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
|
||||||
|
module.exports = async (query, request) => {
|
||||||
|
const deviceName = String(query.deviceName || query.name || '').trim()
|
||||||
|
|
||||||
|
if (!deviceName) {
|
||||||
|
return Promise.reject({
|
||||||
|
status: 400,
|
||||||
|
body: {
|
||||||
|
code: 400,
|
||||||
|
msg: '缺少必要参数: deviceName',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
deviceName,
|
||||||
|
}
|
||||||
|
|
||||||
|
return request(
|
||||||
|
`/api/deviceinfo/center/upload`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'eapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
// 修改本人动态的可见权限
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
|
||||||
|
const PRIVACY_VALUES = new Set([0, 1, 2, 6])
|
||||||
|
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const eventId = String(query.evId ?? '').trim()
|
||||||
|
const rawPrivacy = String(query.privacy ?? '').trim()
|
||||||
|
const privacy = Number(rawPrivacy)
|
||||||
|
|
||||||
|
if (
|
||||||
|
!eventId ||
|
||||||
|
!rawPrivacy ||
|
||||||
|
!Number.isInteger(privacy) ||
|
||||||
|
!PRIVACY_VALUES.has(privacy)
|
||||||
|
) {
|
||||||
|
return Promise.resolve({
|
||||||
|
status: 400,
|
||||||
|
body: {
|
||||||
|
code: 400,
|
||||||
|
message: 'evId is required and privacy must be one of 0, 1, 2, 6',
|
||||||
|
},
|
||||||
|
cookie: [],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
eventId,
|
||||||
|
privacy,
|
||||||
|
}
|
||||||
|
|
||||||
|
return request(`/api/event/privacy/op`, data, createOption(query))
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
// 乐迷团详情
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
groupId: query.groupId || query.id || '',
|
||||||
|
scene: query.scene || '',
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
'/api/social/fansgroup/bff/detail/get',
|
||||||
|
data,
|
||||||
|
createOption(query, 'eapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
// 乐迷团推荐笔记
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
artistSelf: query.artistSelf || '0',
|
||||||
|
fansGroupId: query.fansGroupId || query.groupId || '',
|
||||||
|
cursor: query.cursor || '0',
|
||||||
|
size: String(query.size || 10),
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
'/api/fans/group/feed/recommend/get',
|
||||||
|
data,
|
||||||
|
createOption(query, 'eapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
// 用户所处乐迷团详情
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
groupId: query.groupId || query.id || '',
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
'/api/social/fansgroup/bff/user/group/detail/get',
|
||||||
|
data,
|
||||||
|
createOption(query, 'eapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// 获取当前登录用户加入的全部歌手乐迷团列表
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
return request(
|
||||||
|
'/api/social/fansgroup/bff/user/groups/get',
|
||||||
|
{},
|
||||||
|
createOption(query, 'eapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
// 红心与取消红心歌曲- v1
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
query.like = query.like == 'false' ? false : true
|
||||||
|
const data = {
|
||||||
|
alg: 'itembased',
|
||||||
|
trackId: query.id,
|
||||||
|
like: query.like,
|
||||||
|
time: '3',
|
||||||
|
}
|
||||||
|
return request(`/api/v1/radio/like`, data, createOption(query, 'xeapi', 'v3'))
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
// 听歌足迹 - 歌曲播放排行 (Top20)
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
return request(
|
||||||
|
`/api/content/activity/listen/data/song/play/rank`,
|
||||||
|
{
|
||||||
|
type: query.type || 'month', //周 week 月 month
|
||||||
|
endTime: query.endTime, // 不填就是本周/月的
|
||||||
|
},
|
||||||
|
createOption(query),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ module.exports = async (query, request) => {
|
|||||||
? query.captcha
|
? query.captcha
|
||||||
: query.md5_password || CryptoJS.MD5(query.password).toString(),
|
: query.md5_password || CryptoJS.MD5(query.password).toString(),
|
||||||
remember: 'true',
|
remember: 'true',
|
||||||
|
secureCaptcha: query.sca || '',
|
||||||
}
|
}
|
||||||
let result = await request(
|
let result = await request(
|
||||||
`/api/w/login/cellphone`,
|
`/api/w/login/cellphone`,
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
// 云小编每日抽奖
|
||||||
|
//
|
||||||
|
// activityId:
|
||||||
|
// 默认 6501202
|
||||||
|
//
|
||||||
|
// drawCount:
|
||||||
|
// 默认 1
|
||||||
|
//
|
||||||
|
// checkToken:
|
||||||
|
// 易盾反作弊 Token
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
activityId: query.activityId || '6501202',
|
||||||
|
drawCount: query.drawCount || '1',
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
`/api/middle/play/do/lottery`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'eapi', 'v2'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
// 云小编抽奖剩余次数查询
|
||||||
|
//
|
||||||
|
// activityId:
|
||||||
|
// 默认 6501202
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
activityId: query.activityId || '6501202',
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
`/api/middle/play/lottery/remain/chance`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'eapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,8 +1,5 @@
|
|||||||
// 私人FM - 模式选择
|
// 私人FM - 模式选择
|
||||||
|
|
||||||
// aidj, DEFAULT, FAMILIAR, EXPLORE, SCENE_RCMD ( EXERCISE, FOCUS, NIGHT_EMO )
|
|
||||||
// 来不及解释这几个了
|
|
||||||
|
|
||||||
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,4 +1,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 = {}
|
||||||
|
|||||||
@@ -9,6 +9,6 @@ module.exports = (query, request) => {
|
|||||||
? { checkToken: query.checkToken || APP_CONF.checkToken }
|
? { checkToken: query.checkToken || APP_CONF.checkToken }
|
||||||
: {}),
|
: {}),
|
||||||
}
|
}
|
||||||
query.checkToken = true // 强制开启checkToken
|
query.checkToken = 'v2' // 强制开启checkToken
|
||||||
return request(`/api/playlist/${path}`, data, createOption(query, 'eapi'))
|
return request(`/api/playlist/${path}`, data, createOption(query, 'eapi'))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// 每日风格推荐场景配置列表获取
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {}
|
||||||
|
return request(
|
||||||
|
`/api/homepage/daily/song/config/get`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'xeapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
// 每日风格推荐场景配置设置
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
tags: JSON.stringify({
|
||||||
|
tagIds: String(query.tags).split(',').map(Number),
|
||||||
|
categoryId: Number(query.category),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
console.log(data)
|
||||||
|
return request(
|
||||||
|
`/api/homepage/daily/song/tag/save`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'xeapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// 每日推荐风格歌曲
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {}
|
||||||
|
return request(
|
||||||
|
`/api/homepage/category/daily/song/list`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'xeapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
// 每日推荐歌曲 - v1
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
ispush: query.ispush || false,
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
`/api/v3/discovery/recommend/songs`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'xeapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// 获取游客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')
|
||||||
@@ -37,7 +39,7 @@ module.exports = async (query, request) => {
|
|||||||
let result = await request(
|
let result = await request(
|
||||||
`/api/register/anonimous`,
|
`/api/register/anonimous`,
|
||||||
data,
|
data,
|
||||||
createOption(query, 'weapi'),
|
createOption(query, 'xeapi'),
|
||||||
)
|
)
|
||||||
if (result.body.code === 200) {
|
if (result.body.code === 200) {
|
||||||
result = {
|
result = {
|
||||||
|
|||||||
@@ -0,0 +1,158 @@
|
|||||||
|
// 易盾反作弊 Token 注册端点
|
||||||
|
// 通过易盾官方 Watchman SDK(Web 版,跑在 jsdom 模拟的浏览器环境里)
|
||||||
|
// 实时调用 getToken(businessId) 获取反作弊 token,供后续带 checkToken 的请求使用
|
||||||
|
//
|
||||||
|
// GET /register/checktoken/v2 → 实时获取新 token(不缓存)
|
||||||
|
// POST /register/checktoken/v2 → 实时获取新 token
|
||||||
|
//
|
||||||
|
// 注意:每次获取都不缓存,模拟真实客户端每次请求使用新鲜 token,
|
||||||
|
// 避免反作弊 token 复用触发风控。
|
||||||
|
//
|
||||||
|
// 注:jsdom 固定用 v24(engines >=18),其依赖链为纯 CJS,可被 pkg 静态
|
||||||
|
// 分析自动打包;v30+ 引入纯 ESM 依赖且要求 Node >=22,无法在 CI(18-24) 与
|
||||||
|
// pkg(node18) 目标下运行。
|
||||||
|
//
|
||||||
|
const { JSDOM, VirtualConsole } = require('jsdom')
|
||||||
|
const { default: axios } = require('axios')
|
||||||
|
const { APP_CONF } = require('../util/config.json')
|
||||||
|
const logger = require('../util/logger')
|
||||||
|
|
||||||
|
// 网易云音乐在易盾的 productNumber 与 businessId
|
||||||
|
const PRODUCT_NUMBER = 'YD00000558929251'
|
||||||
|
const BUSINESS_ID = 'bd5d2f973ef74cd2a61325a412ae54d9'
|
||||||
|
const TOOL_JS_URL = `${APP_CONF.dunStaticDomain}/tool.min.js`
|
||||||
|
|
||||||
|
// 最小 HTML 外壳,模拟网页环境
|
||||||
|
const HTML =
|
||||||
|
'<!doctype html><html><head><meta charset="UTF-8"></head><body></body></html>'
|
||||||
|
|
||||||
|
let toolJs = ''
|
||||||
|
let wm = null // Watchman 实例(进程内复用,可反复 getToken)
|
||||||
|
let dom = null // jsdom 实例(失败时 close 释放活动句柄,避免泄漏)
|
||||||
|
let initPromise = null
|
||||||
|
|
||||||
|
// 获取 tool.min.js(内存缓存,避免每次初始化重复下载)
|
||||||
|
async function getToolJs() {
|
||||||
|
if (toolJs) return toolJs
|
||||||
|
const res = await axios.get(TOOL_JS_URL, { timeout: 10000 })
|
||||||
|
toolJs = String(res.data)
|
||||||
|
return toolJs
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化 Watchman(进程内只初始化一次,实例可反复 getToken)
|
||||||
|
async function ensureWatchman() {
|
||||||
|
if (wm) return wm
|
||||||
|
if (initPromise) return initPromise
|
||||||
|
|
||||||
|
initPromise = (async () => {
|
||||||
|
const js = await getToolJs()
|
||||||
|
const virtualConsole = new VirtualConsole()
|
||||||
|
virtualConsole.on('jsdomError', () => {})
|
||||||
|
dom = new JSDOM(HTML, {
|
||||||
|
url: 'https://music.163.com/',
|
||||||
|
referrer: 'https://music.163.com/',
|
||||||
|
contentType: 'text/html',
|
||||||
|
runScripts: 'dangerously',
|
||||||
|
resources: 'usable', // 允许动态加载 watchman.min.js / JSONP
|
||||||
|
pretendToBeVisual: true,
|
||||||
|
virtualConsole,
|
||||||
|
beforeParse(window) {
|
||||||
|
// 抹掉 headless 特征,避免易盾风控误判
|
||||||
|
Object.defineProperty(window.navigator, 'webdriver', {
|
||||||
|
get: () => undefined,
|
||||||
|
})
|
||||||
|
window.chrome = { runtime: {} }
|
||||||
|
window.navigator.languages = ['zh-CN', 'zh']
|
||||||
|
window.navigator.plugins = [1, 2, 3, 4, 5]
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const script = dom.window.document.createElement('script')
|
||||||
|
script.textContent = js
|
||||||
|
dom.window.document.body.appendChild(script)
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let settled = false
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
if (settled) return
|
||||||
|
settled = true
|
||||||
|
reject(new Error('watchman 初始化超时'))
|
||||||
|
}, 15000)
|
||||||
|
dom.window.initWatchman({
|
||||||
|
auto: true,
|
||||||
|
productNumber: PRODUCT_NUMBER,
|
||||||
|
onload(instance) {
|
||||||
|
if (settled) return
|
||||||
|
settled = true
|
||||||
|
clearTimeout(timer)
|
||||||
|
wm = instance
|
||||||
|
resolve(instance)
|
||||||
|
},
|
||||||
|
onerror(...args) {
|
||||||
|
if (settled) return
|
||||||
|
settled = true
|
||||||
|
clearTimeout(timer)
|
||||||
|
reject(new Error('watchman 初始化失败'))
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})()
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await initPromise
|
||||||
|
} catch (e) {
|
||||||
|
// 失败路径:关闭 jsdom 释放 rAF/子资源/定时器等活动句柄,防止每次失败泄漏约 30MB
|
||||||
|
if (dom) {
|
||||||
|
dom.window.close()
|
||||||
|
dom = null
|
||||||
|
}
|
||||||
|
initPromise = null
|
||||||
|
wm = null
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取新 token
|
||||||
|
async function fetchToken() {
|
||||||
|
const instance = await ensureWatchman()
|
||||||
|
const raw = instance.getInstance()
|
||||||
|
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
const timer = setTimeout(() => resolve(), 15000)
|
||||||
|
raw.I(() => {
|
||||||
|
clearTimeout(timer)
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const timer = setTimeout(() => resolve(''), 15000)
|
||||||
|
instance.getToken(BUSINESS_ID, (tk) => {
|
||||||
|
clearTimeout(timer)
|
||||||
|
resolve(typeof tk === 'string' ? tk : '')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 端点处理:每次实时获取新 token
|
||||||
|
module.exports = async () => {
|
||||||
|
let token = ''
|
||||||
|
try {
|
||||||
|
token = await fetchToken()
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn('[checkToken v2]', e.message)
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
body: { code: 200, token, registered: !!token },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 给 request.js 读取用:每次调用实时获取新 token,不缓存
|
||||||
|
module.exports.getToken = async () => {
|
||||||
|
try {
|
||||||
|
return await fetchToken()
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn('[checkToken v2]', e.message)
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
// 易盾反作弊 Token 注册端点
|
||||||
|
// 调用后获取实时 token 并存入共享存储,供后续带 checkToken 的请求使用
|
||||||
|
//
|
||||||
|
// GET /register/checktoken/v3 → 实时获取新 token(不缓存)
|
||||||
|
// POST /register/checktoken/v3 → 实时获取新 token
|
||||||
|
//
|
||||||
|
// 注意:每次获取都不缓存,模拟真实客户端每次请求使用新鲜 token,
|
||||||
|
// 避免反作弊 token 复用触发风控。
|
||||||
|
//
|
||||||
|
const { default: axios } = require('axios')
|
||||||
|
const { APP_CONF } = require('../util/config.json')
|
||||||
|
|
||||||
|
const URL = APP_CONF.dunDomainV3 + '/v3/b?pn=YD00000558929251'
|
||||||
|
|
||||||
|
async function fetch() {
|
||||||
|
const res = await axios.get(URL, { timeout: 10000 })
|
||||||
|
const body = String(res.data)
|
||||||
|
const m = body.match(/null\(\[(\d+),\d+,\"([^\"]+)\"\]\)/)
|
||||||
|
if (m && m[1] === '200') return m[2]
|
||||||
|
throw new Error('易盾返回异常: ' + body.substring(0, 100))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 端点处理:每次实时获取新 token
|
||||||
|
module.exports = async () => {
|
||||||
|
let token = ''
|
||||||
|
try {
|
||||||
|
token = await fetch()
|
||||||
|
} catch (e) {
|
||||||
|
// token 获取失败时返回空,由调用方决定是否重试
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
body: { code: 200, token, registered: !!token },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 给 request.js 读取用:每次调用实时获取新 token,不缓存
|
||||||
|
module.exports.getToken = async () => {
|
||||||
|
try {
|
||||||
|
return await fetch()
|
||||||
|
} catch (e) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
const { default: axios } = require('axios')
|
||||||
|
const { APP_CONF } = require('../util/config.json')
|
||||||
|
const { saveNeapiKey } = require('../util/neapiKey')
|
||||||
|
const { decodeConfig } = require('../util/neapiConfig')
|
||||||
|
|
||||||
|
const USER_AGENT =
|
||||||
|
'NeteaseMusic/9.5.61.260802021928(9005061);Dalvik/2.1.0 (Linux; U; Android 12; HBN-AL00 Build/cd737a2.0)'
|
||||||
|
|
||||||
|
module.exports = async (query, request) => {
|
||||||
|
const version = Number(query.version) || 0
|
||||||
|
const behavior = query.behavior === 'update' ? 'update' : 'open'
|
||||||
|
|
||||||
|
const res = await axios({
|
||||||
|
method: 'GET',
|
||||||
|
url: APP_CONF.apiDomain + '/api/gorilla/algorithm/record',
|
||||||
|
params: { version, behavior },
|
||||||
|
headers: { 'User-Agent': USER_AGENT },
|
||||||
|
proxy: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!res.data || res.data.code !== 200 || !res.data.data) {
|
||||||
|
throw new Error('neapi config request failed')
|
||||||
|
}
|
||||||
|
|
||||||
|
// file 为空表示本地已是最新,无需更新
|
||||||
|
const { file, circleTime } = res.data.data
|
||||||
|
const config = file
|
||||||
|
? saveNeapiKey(decodeConfig(Buffer.from(file, 'base64')), circleTime)
|
||||||
|
: null
|
||||||
|
|
||||||
|
// 只回版本与密钥偏移,密钥本身留在本地缓存
|
||||||
|
const keyOffsets = {}
|
||||||
|
if (config) {
|
||||||
|
keyOffsets.encrypt = config.keyBlob.indexOf(config.encryptKey)
|
||||||
|
keyOffsets.decrypt = config.keyBlob.indexOf(config.decryptKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
version: config ? config.version : res.data.data.version,
|
||||||
|
updated: Boolean(config),
|
||||||
|
circleTime,
|
||||||
|
keyOffsets,
|
||||||
|
},
|
||||||
|
cookie: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
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.5.61',
|
||||||
|
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/bsr/sk/get',
|
||||||
|
headers: {
|
||||||
|
'User-Agent':
|
||||||
|
'NeteaseMusic/9.5.61.260802021928(9005061);Dalvik/2.1.0 (Linux; U; Android 12; HBN-AL00 Build/cd737a2.0)',
|
||||||
|
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: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
// 提交歌曲播放状态
|
||||||
|
|
||||||
|
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'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
// 云小编领取任务积分
|
||||||
|
//
|
||||||
|
// activityId:
|
||||||
|
// 调用 rep/ugc/activity/get 获取
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
activityId: query.activityId || '5001',
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
`/api/rep/ugc/activity/collect`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'eapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
// 云小编活动信息
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
return request(`/api/rep/ugc/activity/get`, {}, createOption(query, 'eapi'))
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
// 云小编考试状态
|
||||||
|
//
|
||||||
|
// examType:
|
||||||
|
// 1. 歌曲曲风审核: musicalStyleEnter
|
||||||
|
// 2. 歌曲语种审核: languageEnter
|
||||||
|
// 3. 歌曲原唱审核: oriSingerEnter
|
||||||
|
// 4. 情绪标签审核: emotionEnter
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
if (!query.examType)
|
||||||
|
return Promise.reject({
|
||||||
|
status: 400,
|
||||||
|
body: {
|
||||||
|
code: 400,
|
||||||
|
message: '参数不足',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const data = {
|
||||||
|
examType: query.examType,
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
'/api/rep/ugc/exam/info/get',
|
||||||
|
data,
|
||||||
|
createOption(query, 'eapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
// 云小编考试取题
|
||||||
|
//
|
||||||
|
// examType:
|
||||||
|
// 1. 歌曲曲风审核: musicalStyleEnter
|
||||||
|
// 2. 歌曲语种审核: languageEnter
|
||||||
|
// 3. 歌曲原唱审核: oriSingerEnter
|
||||||
|
// 4. 情绪标签审核: emotionEnter
|
||||||
|
//
|
||||||
|
// taskId:
|
||||||
|
// 首次调用 rep/ugc/exam/start 获取,之后调用 rep/ugc/exam/info/get 获取
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
if (!query.examType || !query.taskId)
|
||||||
|
return Promise.reject({
|
||||||
|
status: 400,
|
||||||
|
body: {
|
||||||
|
code: 400,
|
||||||
|
message: '参数不足',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const data = {
|
||||||
|
examType: query.examType,
|
||||||
|
taskId: query.taskId,
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
'/api/rep/ugc/exam/question/single/get',
|
||||||
|
data,
|
||||||
|
createOption(query, 'eapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
// 云小编考试结果
|
||||||
|
//
|
||||||
|
// examType:
|
||||||
|
// 1. 歌曲曲风审核: musicalStyleEnter
|
||||||
|
// 2. 歌曲语种审核: languageEnter
|
||||||
|
// 3. 歌曲原唱审核: oriSingerEnter
|
||||||
|
// 4. 情绪标签审核: emotionEnter
|
||||||
|
//
|
||||||
|
// taskId:
|
||||||
|
// 首次调用 rep/ugc/exam/start 获取,之后调用 rep/ugc/exam/info/get 获取
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
if (!query.examType || !query.taskId)
|
||||||
|
return Promise.reject({
|
||||||
|
status: 400,
|
||||||
|
body: {
|
||||||
|
code: 400,
|
||||||
|
message: '参数不足',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const data = {
|
||||||
|
examType: query.examType,
|
||||||
|
taskId: query.taskId,
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
'/api/rep/ugc/exam/result/get',
|
||||||
|
data,
|
||||||
|
createOption(query, 'eapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
// 云小编考试开始
|
||||||
|
//
|
||||||
|
// examType:
|
||||||
|
// 1. 歌曲曲风审核: musicalStyleEnter
|
||||||
|
// 2. 歌曲语种审核: languageEnter
|
||||||
|
// 3. 歌曲原唱审核: oriSingerEnter
|
||||||
|
// 4. 情绪标签审核: emotionEnter
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
if (!query.examType)
|
||||||
|
return Promise.reject({
|
||||||
|
status: 400,
|
||||||
|
body: {
|
||||||
|
code: 400,
|
||||||
|
message: '参数不足',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const data = {
|
||||||
|
examType: query.examType,
|
||||||
|
}
|
||||||
|
return request('/api/rep/ugc/exam/start', data, createOption(query, 'eapi'))
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
// 云小编考试提交
|
||||||
|
//
|
||||||
|
// examType:
|
||||||
|
// 1. 歌曲曲风审核: musicalStyleEnter
|
||||||
|
// 2. 歌曲语种审核: languageEnter
|
||||||
|
// 3. 歌曲原唱审核: oriSingerEnter
|
||||||
|
// 4. 情绪标签审核: emotionEnter
|
||||||
|
//
|
||||||
|
// taskId:
|
||||||
|
// 首次调用 rep/ugc/exam/start 获取,之后调用 rep/ugc/exam/info/get 获取
|
||||||
|
//
|
||||||
|
// questionId:
|
||||||
|
// 调用 rep/ugc/exam/question/single/get 获取
|
||||||
|
//
|
||||||
|
// answer:
|
||||||
|
// A 对, B 错
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
if (!query.examType || !query.taskId || !query.questionId || !query.answer)
|
||||||
|
return Promise.reject({
|
||||||
|
status: 400,
|
||||||
|
body: {
|
||||||
|
code: 400,
|
||||||
|
message: '参数不足',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const data = {
|
||||||
|
examType: query.examType,
|
||||||
|
taskId: query.taskId,
|
||||||
|
questionId: query.questionId,
|
||||||
|
answer: query.answer,
|
||||||
|
}
|
||||||
|
return request('/api/rep/ugc/exam/submit', data, createOption(query, 'eapi'))
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
// 云小编领取一日会员
|
||||||
|
//
|
||||||
|
// 注:前提条件见 rep/ugc/user/vip
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
activityId: query.activityId || '5001',
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
`/api/rep/ugc/user/collect-vip`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'eapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
// 云小编获取用户详情
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
return request(`/api/rep/ugc/user/get`, {}, createOption(query, 'eapi'))
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
// 云小编每日签到
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
return request(`/api/rep/ugc/user/sign`, {}, createOption(query, 'eapi'))
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
// 云小编查询会员任务状态
|
||||||
|
//
|
||||||
|
// 状态 (data.status)
|
||||||
|
// 10: 用户积分达50,可免费领取1日黑胶会员
|
||||||
|
// 20: 用户积分已达50,可免费领取1日黑胶会员
|
||||||
|
// 30: 已领取1日黑胶会员,明天再来吧~
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
return request(`/api/rep/ugc/user/vip`, {}, createOption(query, 'eapi'))
|
||||||
|
}
|
||||||
+57
-5
@@ -1,8 +1,42 @@
|
|||||||
// 听歌打卡
|
// 听歌打卡
|
||||||
|
|
||||||
const createOption = require('../util/option.js')
|
const createOption = require('../util/option.js')
|
||||||
module.exports = (query, request) => {
|
const { APP_CONF } = require('../util/config.json')
|
||||||
const data = {
|
const DOMAIN = APP_CONF.clDomian
|
||||||
|
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',
|
||||||
@@ -15,12 +49,30 @@ module.exports = (query, request) => {
|
|||||||
type: 'song',
|
type: 'song',
|
||||||
wifi: 0,
|
wifi: 0,
|
||||||
source: 'list',
|
source: 'list',
|
||||||
mainsite: 1,
|
mainsite: '1',
|
||||||
content: '',
|
mainsiteWeb: '1',
|
||||||
|
content: `id=${query.sourceid}`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
}
|
}
|
||||||
|
|
||||||
return request(`/api/feedback/weblog`, data, createOption(query, 'weapi'))
|
const option = createOption(query, 'eapi')
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,128 @@
|
|||||||
|
// 听歌打卡 - 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}` },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,5 +7,9 @@ module.exports = (query, request) => {
|
|||||||
msg: query.msg || '',
|
msg: query.msg || '',
|
||||||
id: query.id || '',
|
id: query.id || '',
|
||||||
}
|
}
|
||||||
return request(`/api/share/friends/resource`, data, createOption(query))
|
return request(
|
||||||
|
`/api/share/friends/resource`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'xeapi', 'v3'),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
// 从云盘获取歌曲下载链接
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
songId: query.id,
|
||||||
|
}
|
||||||
|
return request(`/api/cloud/dowonload`, data, createOption(query, 'eapi'))
|
||||||
|
}
|
||||||
@@ -1,13 +1,23 @@
|
|||||||
// 获取客户端歌曲下载链接 - v1
|
// 获取客户端歌曲下载链接 - v1
|
||||||
// 此版本不再采用 br 作为音质区分的标准
|
// 此版本不再采用 br 作为音质区分的标准
|
||||||
// 而是采用 standard, exhigh, lossless, hires, jyeffect(高清环绕声), sky(沉浸环绕声), jymaster(超清母带) 进行音质判断
|
// 而是采用 standard, exhigh, lossless, hires, jyeffect(高清臻音), vivid(臻音全景声), jymaster(超清母带), sky(沉浸环绕声) 进行音质判断
|
||||||
|
|
||||||
const createOption = require('../util/option.js')
|
const createOption = require('../util/option.js')
|
||||||
|
const { cookieToJson } = require('../util/index.js')
|
||||||
module.exports = (query, request) => {
|
module.exports = (query, request) => {
|
||||||
const data = {
|
const data = {
|
||||||
id: query.id,
|
id: query.id,
|
||||||
immerseType: 'c51',
|
immerseType: 'c51',
|
||||||
level: query.level,
|
level: query.level,
|
||||||
}
|
}
|
||||||
return request(`/api/song/enhance/download/url/v1`, data, createOption(query))
|
const options = createOption(query)
|
||||||
|
if (query.level === 'vivid') {
|
||||||
|
const cookie = options.cookie
|
||||||
|
options.cookie = {
|
||||||
|
...(typeof cookie === 'string' ? cookieToJson(cookie) : cookie),
|
||||||
|
os: 'android',
|
||||||
|
appver: '9.5.61',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return request(`/api/song/enhance/download/url/v1`, data, options)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
// 插播相似歌曲
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
positionCode: 'toolBarRcmdSong',
|
||||||
|
resourceId: query.id,
|
||||||
|
resourceType: 'song',
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
`/api/link/position/show/resource`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'eapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
+15
-7
@@ -1,10 +1,12 @@
|
|||||||
// 歌曲链接 - v1
|
// 歌曲链接 - v1
|
||||||
// 此版本不再采用 br 作为音质区分的标准
|
// 此版本不再采用 br 作为音质区分的标准
|
||||||
// 而是采用 standard, exhigh, lossless, hires, jyeffect(高清环绕声), sky(沉浸环绕声), jymaster(超清母带) 进行音质判断
|
// 而是采用 standard, exhigh, lossless, hires, jyeffect(高清臻音), vivid(臻音全景声), jymaster(超清母带), sky(沉浸环绕声) 进行音质判断
|
||||||
// 当unblock为true时, 会尝试使用unblockmusic-utils进行解锁, 同时音质设置不会生效, 但仍然为必须传入参数
|
// 当unblock为true时, 会尝试使用unblockmusic-utils进行解锁, 同时音质设置不会生效, 但仍然为必须传入参数
|
||||||
|
// 当level为sky时, 可通过 immerseType 选择沉浸声类型, 支持 c512(新版c51类型)、ste2(新版环绕立体声类型)、aac2(新版aac类型)、c51(c51类型)、ste(环绕立体声类型)、aac(aac类型), 默认为 c51
|
||||||
|
|
||||||
const logger = require('../util/logger.js')
|
const logger = require('../util/logger.js')
|
||||||
const createOption = require('../util/option.js')
|
const createOption = require('../util/option.js')
|
||||||
|
const { cookieToJson } = require('../util/index.js')
|
||||||
module.exports = async (query, request) => {
|
module.exports = async (query, request) => {
|
||||||
const {
|
const {
|
||||||
matchID,
|
matchID,
|
||||||
@@ -15,6 +17,7 @@ module.exports = async (query, request) => {
|
|||||||
level: query.level,
|
level: query.level,
|
||||||
encodeType: 'flac',
|
encodeType: 'flac',
|
||||||
}
|
}
|
||||||
|
const options = createOption(query, 'xeapi')
|
||||||
if (query.unblock === 'true') {
|
if (query.unblock === 'true') {
|
||||||
try {
|
try {
|
||||||
const result = await matchID(query.id, query.source)
|
const result = await matchID(query.id, query.source)
|
||||||
@@ -51,11 +54,16 @@ module.exports = async (query, request) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (data.level == 'sky') {
|
if (data.level == 'sky') {
|
||||||
data.immerseType = 'c51'
|
data.immerseType = query.immerseType || 'c51'
|
||||||
}
|
}
|
||||||
return request(
|
if (data.level == 'vivid') {
|
||||||
`/api/song/enhance/player/url/v1`,
|
data.encodeType = 'mp3'
|
||||||
data,
|
const cookie = options.cookie
|
||||||
createOption(query, 'xeapi'),
|
options.cookie = {
|
||||||
)
|
...(typeof cookie === 'string' ? cookieToJson(cookie) : cookie),
|
||||||
|
os: 'android',
|
||||||
|
appver: '9.5.61',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return request(`/api/song/enhance/player/url/v1`, data, options)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,30 @@
|
|||||||
// 获取客户端歌曲下载链接 - v1
|
// 获取客户端歌曲下载链接 - v1
|
||||||
// 此版本不再采用 br 作为音质区分的标准
|
// 此版本不再采用 br 作为音质区分的标准
|
||||||
// 而是采用 standard, exhigh, lossless, hires, jyeffect(高清环绕声), sky(沉浸环绕声), jymaster(超清母带) 进行音质判断
|
// 而是采用 standard, exhigh, lossless, hires, jyeffect(高清臻音), vivid(臻音全景声), jymaster(超清母带), sky(沉浸环绕声) 进行音质判断
|
||||||
|
|
||||||
const createOption = require('../util/option.js')
|
const createOption = require('../util/option.js')
|
||||||
|
const { cookieToJson } = require('../util/index.js')
|
||||||
module.exports = async (query, request) => {
|
module.exports = async (query, request) => {
|
||||||
const data = {
|
const data = {
|
||||||
id: query.id,
|
id: query.id,
|
||||||
immerseType: 'c51',
|
immerseType: 'c51',
|
||||||
level: query.level,
|
level: query.level,
|
||||||
}
|
}
|
||||||
|
const options = createOption(query)
|
||||||
|
if (query.level === 'vivid') {
|
||||||
|
const cookie = options.cookie
|
||||||
|
options.cookie = {
|
||||||
|
...(typeof cookie === 'string' ? cookieToJson(cookie) : cookie),
|
||||||
|
os: 'android',
|
||||||
|
appver: '9.5.61',
|
||||||
|
}
|
||||||
|
}
|
||||||
const response = await request(
|
const response = await request(
|
||||||
`/api/song/enhance/download/url/v1`,
|
`/api/song/enhance/download/url/v1`,
|
||||||
data,
|
data,
|
||||||
createOption(query),
|
options,
|
||||||
)
|
)
|
||||||
let url = response?.body?.data?.[0]?.url
|
let url = response?.body?.data?.url || response?.body?.data?.[0]?.url
|
||||||
|
|
||||||
if (!url) {
|
if (!url) {
|
||||||
const fallbackData = {
|
const fallbackData = {
|
||||||
@@ -25,10 +35,13 @@ module.exports = async (query, request) => {
|
|||||||
if (query.level === 'sky') {
|
if (query.level === 'sky') {
|
||||||
fallbackData.immerseType = 'c51'
|
fallbackData.immerseType = 'c51'
|
||||||
}
|
}
|
||||||
|
if (query.level === 'vivid') {
|
||||||
|
fallbackData.encodeType = 'mp3'
|
||||||
|
}
|
||||||
const fallback = await request(
|
const fallback = await request(
|
||||||
`/api/song/enhance/player/url/v1`,
|
`/api/song/enhance/player/url/v1`,
|
||||||
fallbackData,
|
fallbackData,
|
||||||
createOption(query),
|
options,
|
||||||
)
|
)
|
||||||
url = fallback?.body?.data?.[0]?.url
|
url = fallback?.body?.data?.[0]?.url
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
// 歌曲百科
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const extJson = {
|
||||||
|
states: {
|
||||||
|
playingResource: {
|
||||||
|
current: query.id,
|
||||||
|
scene: 'songWiki',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const data = {
|
||||||
|
extJson: JSON.stringify(extJson),
|
||||||
|
positionCode: 'songWikiMainPosition',
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
`/api/link/page/parent/relation/construct/info`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'eapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
// 云小编获取任务
|
||||||
|
//
|
||||||
|
// type:
|
||||||
|
// 1: 歌曲曲风审核 musicalStyleEnter
|
||||||
|
// 2: 歌曲语种审核 languageEnter
|
||||||
|
// 3: 歌曲原唱审核 oriSingerEnter
|
||||||
|
// 4: 情绪标签审核 emotionEnter
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
type: query.type || '4',
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
`/api/thinktank/audit/resource/detail`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'eapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
// 云小编提交任务
|
||||||
|
//
|
||||||
|
// type:
|
||||||
|
// 1: 歌曲曲风审核 musicalStyleEnter
|
||||||
|
// 2: 歌曲语种审核 languageEnter
|
||||||
|
// 3: 歌曲原唱审核 oriSingerEnter
|
||||||
|
// 4: 情绪标签审核 emotionEnter
|
||||||
|
//
|
||||||
|
// taskId:
|
||||||
|
// 调用 thinktank/audit/resource/detail 获取
|
||||||
|
//
|
||||||
|
// judgement:
|
||||||
|
// 1: 同意, 2: 否决, 3: 跳过 (不算次数)
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
if (!query.taskId || !query.judgement)
|
||||||
|
return Promise.reject({
|
||||||
|
status: 400,
|
||||||
|
body: {
|
||||||
|
code: 400,
|
||||||
|
message: '参数不足',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const data = {
|
||||||
|
type: query.type || '4',
|
||||||
|
taskId: query.taskId,
|
||||||
|
judgement: query.judgement,
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
`/api/thinktank/audit/resource/update`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'eapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -4,9 +4,10 @@ const createOption = require('../util/option.js')
|
|||||||
module.exports = (query, request) => {
|
module.exports = (query, request) => {
|
||||||
const data = {
|
const data = {
|
||||||
getcounts: true,
|
getcounts: true,
|
||||||
time: query.lasttime || -1,
|
time: query.lasttime ?? -1,
|
||||||
limit: query.limit || 30,
|
limit: query.limit ?? 30,
|
||||||
total: false,
|
total: false,
|
||||||
|
fromRN: 'true',
|
||||||
}
|
}
|
||||||
return request(`/api/event/get/${query.uid}`, data, createOption(query))
|
return request(`/api/event/get/${query.uid}`, data, createOption(query))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
// 获取当前登录用户可被上游枚举的全部动态
|
||||||
|
|
||||||
|
const userAccount = require('./user_account.js')
|
||||||
|
const userEvent = require('./user_event.js')
|
||||||
|
|
||||||
|
const PAGE_SIZE = 100
|
||||||
|
const MAX_PAGES = 1000
|
||||||
|
|
||||||
|
const errorResponse = (message, cookie = []) => ({
|
||||||
|
status: 502,
|
||||||
|
body: {
|
||||||
|
code: 502,
|
||||||
|
message,
|
||||||
|
},
|
||||||
|
cookie,
|
||||||
|
})
|
||||||
|
|
||||||
|
module.exports = async (query, request) => {
|
||||||
|
const accountResult = await userAccount(query, request)
|
||||||
|
const uid =
|
||||||
|
accountResult.body?.account?.id || accountResult.body?.profile?.userId
|
||||||
|
|
||||||
|
if (!uid) {
|
||||||
|
if (
|
||||||
|
accountResult.status !== 200 ||
|
||||||
|
(accountResult.body?.code && accountResult.body.code !== 200)
|
||||||
|
) {
|
||||||
|
return accountResult
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: 401,
|
||||||
|
body: {
|
||||||
|
code: 401,
|
||||||
|
message: 'A valid login cookie is required',
|
||||||
|
},
|
||||||
|
cookie: accountResult.cookie || [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cookies = [...(accountResult.cookie || [])]
|
||||||
|
const events = []
|
||||||
|
const eventIds = new Set()
|
||||||
|
const cursors = new Set()
|
||||||
|
let lasttime = -1
|
||||||
|
let more = true
|
||||||
|
let pageCount = 0
|
||||||
|
let size = null
|
||||||
|
|
||||||
|
while (more) {
|
||||||
|
pageCount += 1
|
||||||
|
|
||||||
|
const pageResult = await userEvent(
|
||||||
|
{
|
||||||
|
...query,
|
||||||
|
uid,
|
||||||
|
lasttime,
|
||||||
|
limit: PAGE_SIZE,
|
||||||
|
},
|
||||||
|
request,
|
||||||
|
)
|
||||||
|
|
||||||
|
cookies.push(...(pageResult.cookie || []))
|
||||||
|
|
||||||
|
if (pageResult.status !== 200 || pageResult.body?.code !== 200) {
|
||||||
|
return {
|
||||||
|
...pageResult,
|
||||||
|
cookie: cookies,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pageCount === 1) {
|
||||||
|
const reportedSize = pageResult.body.size
|
||||||
|
const numericSize = Number(reportedSize)
|
||||||
|
size =
|
||||||
|
reportedSize != null && Number.isFinite(numericSize)
|
||||||
|
? numericSize
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const event of pageResult.body.events || []) {
|
||||||
|
if (event?.id == null) {
|
||||||
|
events.push(event)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventId = String(event.id)
|
||||||
|
if (!eventIds.has(eventId)) {
|
||||||
|
eventIds.add(eventId)
|
||||||
|
events.push(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
more = Boolean(pageResult.body.more)
|
||||||
|
lasttime = pageResult.body.lasttime
|
||||||
|
|
||||||
|
if (more) {
|
||||||
|
const cursor = String(lasttime ?? '')
|
||||||
|
if (!cursor || cursors.has(cursor)) {
|
||||||
|
return errorResponse(
|
||||||
|
'Upstream event pagination cursor stalled',
|
||||||
|
cookies,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
cursors.add(cursor)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (more && pageCount >= MAX_PAGES) {
|
||||||
|
return errorResponse(
|
||||||
|
`Upstream event pagination exceeded ${MAX_PAGES} pages`,
|
||||||
|
cookies,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const retrievedCount = events.length
|
||||||
|
const unavailableCount =
|
||||||
|
size == null ? null : Math.max(size - retrievedCount, 0)
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
code: 200,
|
||||||
|
events,
|
||||||
|
size,
|
||||||
|
retrievedCount,
|
||||||
|
unavailableCount,
|
||||||
|
sizeMismatch: size == null ? null : size !== retrievedCount,
|
||||||
|
pageCount,
|
||||||
|
more: false,
|
||||||
|
lasttime: lasttime ?? null,
|
||||||
|
},
|
||||||
|
cookie: cookies,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// 一键领取所有会员成长值
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {}
|
||||||
|
return request(
|
||||||
|
`/api/vipnewcenter/app/level/task/reward/getall`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'xeapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
+36
-3
@@ -1,7 +1,40 @@
|
|||||||
// 黑胶乐签打卡
|
// 黑胶乐签打卡
|
||||||
|
|
||||||
const createOption = require('../util/option.js')
|
const createOption = require('../util/option.js')
|
||||||
module.exports = (query, request) => {
|
|
||||||
const data = {}
|
module.exports = async (query, request) => {
|
||||||
return request(`/api/vip-center-bff/task/sign`, data, createOption(query))
|
const results = {}
|
||||||
|
|
||||||
|
const taskSign = await request(
|
||||||
|
'/api/vip-center-bff/task/sign',
|
||||||
|
{},
|
||||||
|
createOption(query, 'weapi'),
|
||||||
|
)
|
||||||
|
results.taskSign = taskSign.body
|
||||||
|
|
||||||
|
const checkinDetail = await request(
|
||||||
|
'/api/vipnewcenter/app/level/user/checkin/history/detail',
|
||||||
|
{
|
||||||
|
signDayTime: Date.now(),
|
||||||
|
type: 1,
|
||||||
|
},
|
||||||
|
createOption(query, 'eapi'),
|
||||||
|
)
|
||||||
|
results.checkinDetail = checkinDetail.body
|
||||||
|
|
||||||
|
// 两个接口都返回 code=200 即视为打卡成功
|
||||||
|
const signed =
|
||||||
|
Number(results.taskSign?.code) === 200 &&
|
||||||
|
Number(results.checkinDetail?.code) === 200
|
||||||
|
|
||||||
|
return {
|
||||||
|
body: {
|
||||||
|
code: 200,
|
||||||
|
...results,
|
||||||
|
signed,
|
||||||
|
message: signed ? '黑胶乐签打卡成功' : '黑胶乐签打卡失败',
|
||||||
|
},
|
||||||
|
cookie: taskSign.cookie,
|
||||||
|
status: 200,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
// 黑胶乐签打卡详情
|
||||||
|
|
||||||
|
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'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
// 黑胶乐签打卡历史 / 状态查询
|
||||||
|
// 支持传入 type=0(用户信息栏)或 type=1(黑胶乐签)
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
type: query.type || '0',
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
`/api/vipnewcenter/app/minidesk/music/sign/pc`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'eapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
// 黑胶乐签签到信息
|
// 黑胶乐签未来签到信息
|
||||||
|
|
||||||
const createOption = require('../util/option.js')
|
const createOption = require('../util/option.js')
|
||||||
module.exports = (query, request) => {
|
module.exports = (query, request) => {
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
// 会员任务 - 新版
|
||||||
|
|
||||||
|
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'),
|
||||||
|
)
|
||||||
|
}
|
||||||
+38
-50
@@ -2,6 +2,7 @@ const { default: axios } = require('axios')
|
|||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
var xml2js = require('xml2js')
|
var xml2js = require('xml2js')
|
||||||
|
|
||||||
|
const uploadPlugin = require('../plugins/upload')
|
||||||
const createOption = require('../util/option.js')
|
const createOption = require('../util/option.js')
|
||||||
const { getFileExtension, readFileChunk } = require('../util/fileHelper')
|
const { getFileExtension, readFileChunk } = require('../util/fileHelper')
|
||||||
|
|
||||||
@@ -19,15 +20,7 @@ function createDupkey() {
|
|||||||
return s.join('')
|
return s.join('')
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = async (query, request) => {
|
module.exports = async (query, request, dependencies = {}) => {
|
||||||
const ext = getFileExtension(query.songFile.name)
|
|
||||||
const filename =
|
|
||||||
query.songName ||
|
|
||||||
query.songFile.name
|
|
||||||
.replace('.' + ext, '')
|
|
||||||
.replace(/\s/g, '')
|
|
||||||
.replace(/\./g, '_')
|
|
||||||
|
|
||||||
if (!query.songFile) {
|
if (!query.songFile) {
|
||||||
return Promise.reject({
|
return Promise.reject({
|
||||||
status: 500,
|
status: 500,
|
||||||
@@ -38,6 +31,19 @@ module.exports = async (query, request) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const axiosRequest = dependencies.axios || axios
|
||||||
|
const uploadImage = dependencies.uploadPlugin || uploadPlugin
|
||||||
|
const ext = getFileExtension(query.songFile.name)
|
||||||
|
const filename =
|
||||||
|
query.songName ||
|
||||||
|
query.songFile.name
|
||||||
|
.replace('.' + ext, '')
|
||||||
|
.replace(/\s/g, '')
|
||||||
|
.replace(/\./g, '_')
|
||||||
|
const coverImgId = query.imgFile
|
||||||
|
? (await uploadImage(query, request)).imgId
|
||||||
|
: query.coverImgId
|
||||||
|
|
||||||
const tokenRes = await request(
|
const tokenRes = await request(
|
||||||
`/api/nos/token/alloc`,
|
`/api/nos/token/alloc`,
|
||||||
{
|
{
|
||||||
@@ -53,7 +59,7 @@ module.exports = async (query, request) => {
|
|||||||
|
|
||||||
const objectKey = tokenRes.body.result.objectKey.replace(/\//g, '%2F')
|
const objectKey = tokenRes.body.result.objectKey.replace(/\//g, '%2F')
|
||||||
const docId = tokenRes.body.result.docId
|
const docId = tokenRes.body.result.docId
|
||||||
const res = await axios({
|
const res = await axiosRequest({
|
||||||
method: 'post',
|
method: 'post',
|
||||||
url: `https://ymusic.nos-hz.163yun.com/${objectKey}?uploads`,
|
url: `https://ymusic.nos-hz.163yun.com/${objectKey}?uploads`,
|
||||||
headers: {
|
headers: {
|
||||||
@@ -94,7 +100,7 @@ module.exports = async (query, request) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const res3 = await axios({
|
const res3 = await axiosRequest({
|
||||||
method: 'put',
|
method: 'put',
|
||||||
url: `https://ymusic.nos-hz.163yun.com/${objectKey}?partNumber=${blockIndex}&uploadId=${res2.InitiateMultipartUploadResult.UploadId[0]}`,
|
url: `https://ymusic.nos-hz.163yun.com/${objectKey}?partNumber=${blockIndex}&uploadId=${res2.InitiateMultipartUploadResult.UploadId[0]}`,
|
||||||
headers: {
|
headers: {
|
||||||
@@ -117,7 +123,7 @@ module.exports = async (query, request) => {
|
|||||||
}
|
}
|
||||||
completeStr += '</CompleteMultipartUpload>'
|
completeStr += '</CompleteMultipartUpload>'
|
||||||
|
|
||||||
await axios({
|
await axiosRequest({
|
||||||
method: 'post',
|
method: 'post',
|
||||||
url: `https://ymusic.nos-hz.163yun.com/${objectKey}?uploadId=${res2.InitiateMultipartUploadResult.UploadId[0]}`,
|
url: `https://ymusic.nos-hz.163yun.com/${objectKey}?uploadId=${res2.InitiateMultipartUploadResult.UploadId[0]}`,
|
||||||
headers: {
|
headers: {
|
||||||
@@ -128,29 +134,29 @@ module.exports = async (query, request) => {
|
|||||||
data: completeStr,
|
data: completeStr,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const voiceData = JSON.stringify([
|
||||||
|
{
|
||||||
|
name: filename,
|
||||||
|
autoPublish: query.autoPublish == 1 ? true : false,
|
||||||
|
autoPublishText: query.autoPublishText || '',
|
||||||
|
description: query.description,
|
||||||
|
voiceListId: query.voiceListId,
|
||||||
|
coverImgId,
|
||||||
|
dfsId: docId,
|
||||||
|
categoryId: query.categoryId,
|
||||||
|
secondCategoryId: query.secondCategoryId,
|
||||||
|
composedSongs: query.composedSongs ? query.composedSongs.split(',') : [],
|
||||||
|
privacy: query.privacy == 1 ? true : false,
|
||||||
|
publishTime: query.publishTime || 0,
|
||||||
|
orderNo: query.orderNo || 1,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
await request(
|
await request(
|
||||||
`/api/voice/workbench/voice/batch/upload/preCheck`,
|
`/api/voice/workbench/voice/batch/upload/preCheck`,
|
||||||
{
|
{
|
||||||
dupkey: createDupkey(),
|
dupkey: createDupkey(),
|
||||||
voiceData: JSON.stringify([
|
voiceData,
|
||||||
{
|
|
||||||
name: filename,
|
|
||||||
autoPublish: query.autoPublish == 1 ? true : false,
|
|
||||||
autoPublishText: query.autoPublishText || '',
|
|
||||||
description: query.description,
|
|
||||||
voiceListId: query.voiceListId,
|
|
||||||
coverImgId: query.coverImgId,
|
|
||||||
dfsId: docId,
|
|
||||||
categoryId: query.categoryId,
|
|
||||||
secondCategoryId: query.secondCategoryId,
|
|
||||||
composedSongs: query.composedSongs
|
|
||||||
? query.composedSongs.split(',')
|
|
||||||
: [],
|
|
||||||
privacy: query.privacy == 1 ? true : false,
|
|
||||||
publishTime: query.publishTime || 0,
|
|
||||||
orderNo: query.orderNo || 1,
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
...createOption(query),
|
...createOption(query),
|
||||||
@@ -163,25 +169,7 @@ module.exports = async (query, request) => {
|
|||||||
`/api/voice/workbench/voice/batch/upload/v2`,
|
`/api/voice/workbench/voice/batch/upload/v2`,
|
||||||
{
|
{
|
||||||
dupkey: createDupkey(),
|
dupkey: createDupkey(),
|
||||||
voiceData: JSON.stringify([
|
voiceData,
|
||||||
{
|
|
||||||
name: filename,
|
|
||||||
autoPublish: query.autoPublish == 1 ? true : false,
|
|
||||||
autoPublishText: query.autoPublishText || '',
|
|
||||||
description: query.description,
|
|
||||||
voiceListId: query.voiceListId,
|
|
||||||
coverImgId: query.coverImgId,
|
|
||||||
dfsId: docId,
|
|
||||||
categoryId: query.categoryId,
|
|
||||||
secondCategoryId: query.secondCategoryId,
|
|
||||||
composedSongs: query.composedSongs
|
|
||||||
? query.composedSongs.split(',')
|
|
||||||
: [],
|
|
||||||
privacy: query.privacy == 1 ? true : false,
|
|
||||||
publishTime: query.publishTime || 0,
|
|
||||||
orderNo: query.orderNo || 1,
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
...createOption(query),
|
...createOption(query),
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
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(`/api/pointmall/user/sign`, data, createOption(query, 'weapi'))
|
return request(
|
||||||
|
`/api/pointmall/user/sign`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'xeapi', 'v3'),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
// 云贝广告任务 - 完成任务领取云贝
|
||||||
|
// 逆向来源: 云贝任务中心 H5 (st.music.163.com/yunbei-listen) main.js
|
||||||
|
// POST /api/ad/power/yunbei/distribution/create
|
||||||
|
// 参数: yunbeiAmount (单次可得云贝, 客户端从 list 接口的 singleAmount 取值, 当前为 150)
|
||||||
|
// 返回: true (领取成功)
|
||||||
|
//
|
||||||
|
// 实测验证 (2026-08-05):
|
||||||
|
// - 仅传 yunbeiAmount 即可成功领取, 无需真实听歌/看视频
|
||||||
|
// - 单日上限 10 次 × 150 云贝 = 1500 云贝/天
|
||||||
|
// - 超限返回 code:400 "单日完成任务数已达上限"
|
||||||
|
// - 无频率限制, 800ms 间隔连续调用均成功
|
||||||
|
//
|
||||||
|
// 建议: 领取前先调 yunbei_ad_task_list 查询 times, 达到 10 次即停止
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
yunbeiAmount: query.yunbeiAmount || 150,
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
`/api/ad/power/yunbei/distribution/create`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'weapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
// 云贝广告任务 - 查询今日任务状态
|
||||||
|
// 逆向来源: 云贝任务中心 H5 (st.music.163.com/yunbei-listen) main.js
|
||||||
|
// GET /api/ad/power/yunbei/distribution/list
|
||||||
|
// 返回: { times: 今日已完成次数, amount: 今日累计云贝, singleAmount: 单次可得云贝 }
|
||||||
|
// 注意: 单日上限 10 次, 单次 150 云贝 (每天最多 1500)
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {}
|
||||||
|
return request(
|
||||||
|
`/api/ad/power/yunbei/distribution/list`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'weapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
// 云贝广告任务 - 获取推荐歌曲
|
||||||
|
// 逆向来源: 云贝任务中心 H5 (st.music.163.com/yunbei-listen) main.js
|
||||||
|
// POST /api/ad/power/yunbei/distribution/recommend/song
|
||||||
|
// 参数: offset (默认 0), limit (默认 10, 客户端一次 10 首)
|
||||||
|
// 返回: 推荐歌曲数组 [{ songId, songName, artistName, albumUrl, songChorusStartTime, likeFlag, alg }]
|
||||||
|
// 注意: alg 均为 alg_payrec_yunBei_*, 为"听歌得云贝"任务专属推荐
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
offset: query.offset || 0,
|
||||||
|
limit: query.limit || 10,
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
`/api/ad/power/yunbei/distribution/recommend/song`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'weapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
import { banner, lyric } from '@neteasecloudmusicapienhanced/api'
|
import { banner, lyric } from '@neteasecloudmusicapienhanced/api'
|
||||||
import { logger } from '../util/logger.js'
|
import type { Response } from '@neteasecloudmusicapienhanced/api'
|
||||||
banner({ type: 0 }).then((res) => {
|
import logger from '../util/logger.js'
|
||||||
|
banner({ type: 0 }).then((res: Response) => {
|
||||||
logger.info(res)
|
logger.info(res)
|
||||||
})
|
})
|
||||||
lyric({
|
lyric({
|
||||||
id: '33894312',
|
id: '33894312',
|
||||||
}).then((res) => {
|
}).then((res: Response) => {
|
||||||
logger.info(res)
|
logger.info(res)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
const {
|
||||||
|
login_cellphone,
|
||||||
|
yunbei_ad_task_list,
|
||||||
|
yunbei_ad_task_recommend_song,
|
||||||
|
yunbei_ad_task_finish,
|
||||||
|
} = require('../main')
|
||||||
|
|
||||||
|
// 云贝广告任务(听歌/看视频得云贝)完整流程示例
|
||||||
|
// 1. list - 查询今日任务状态(次数/云贝)
|
||||||
|
// 2. finish - 领取云贝, 仅需传 yunbeiAmount(单次 150), 无需真实听歌/看视频
|
||||||
|
// 3. recommend/song - 可选, 获取推荐歌曲列表
|
||||||
|
// 注意: 单日上限 10 次 x 150 = 1500 云贝/天
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const login = await login_cellphone({
|
||||||
|
phone: '手机号',
|
||||||
|
password: '密码',
|
||||||
|
})
|
||||||
|
const cookie = login.body.cookie
|
||||||
|
|
||||||
|
// 1. 查询今日任务状态
|
||||||
|
const list = await yunbei_ad_task_list({ cookie })
|
||||||
|
const { times, amount, singleAmount } = list.body
|
||||||
|
console.log(
|
||||||
|
`今日已完成 ${times} 次, 累计 ${amount} 云贝, 单次可得 ${singleAmount} 云贝`,
|
||||||
|
)
|
||||||
|
if (times >= 10) {
|
||||||
|
console.log('已达单日上限, 明天再来吧')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 领取云贝(单次 150)
|
||||||
|
const finish = await yunbei_ad_task_finish({
|
||||||
|
yunbeiAmount: singleAmount,
|
||||||
|
cookie,
|
||||||
|
})
|
||||||
|
console.log('领取结果:', finish.body)
|
||||||
|
|
||||||
|
// 3. 获取推荐歌曲(听歌任务专属推荐, 可选)
|
||||||
|
const rcmd = await yunbei_ad_task_recommend_song({
|
||||||
|
offset: 0,
|
||||||
|
limit: 10,
|
||||||
|
cookie,
|
||||||
|
})
|
||||||
|
console.log('推荐歌曲:', rcmd.body.map((s) => s.songName).join(', '))
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
+35
-19
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@neteasecloudmusicapienhanced/api",
|
"name": "@neteasecloudmusicapienhanced/api",
|
||||||
"version": "4.34.0",
|
"version": "4.41.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)",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "nodemon app.js",
|
"dev": "nodemon app.js",
|
||||||
"start": "node app.js",
|
"start": "node app.js",
|
||||||
@@ -9,9 +9,11 @@
|
|||||||
"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",
|
||||||
"pkgwin": "pkg . -t node18-win-x64 -C GZip -o precompiled/app",
|
"docs:format": "node scripts/format-docs.js",
|
||||||
"pkglinux": "pkg . -t node18-linux-x64 -C GZip -o precompiled/app",
|
"docs:check": "node scripts/format-docs.js --check",
|
||||||
"pkgmacos": "pkg . -t node18-macos-x64 -C GZip -o precompiled/app"
|
"pkgwin": "node scripts/build-pkg.js . -t node18-win-x64 -C GZip -o precompiled/app",
|
||||||
|
"pkglinux": "node scripts/build-pkg.js . -t node18-linux-x64 -C GZip -o precompiled/app",
|
||||||
|
"pkgmacos": "node scripts/build-pkg.js . -t node18-macos-x64 -C GZip -o precompiled/app"
|
||||||
},
|
},
|
||||||
"bin": "./app.js",
|
"bin": "./app.js",
|
||||||
"pkg": {
|
"pkg": {
|
||||||
@@ -41,6 +43,14 @@
|
|||||||
],
|
],
|
||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -65,44 +75,50 @@
|
|||||||
"data"
|
"data"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@neteasecloudmusicapienhanced/unblockmusic-utils": "^0.3.2",
|
"@neteasecloudmusicapienhanced/unblockmusic-utils": "^0.4.5",
|
||||||
"axios": "^1.16.1",
|
"axios": "^1.20.0",
|
||||||
"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",
|
||||||
|
"fzstd": "^0.1.1",
|
||||||
"gzip": "^0.1.0",
|
"gzip": "^0.1.0",
|
||||||
"music-metadata": "^11.12.3",
|
"jsdom": "^24.1.3",
|
||||||
|
"music-metadata": "^11.16.1",
|
||||||
"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",
|
||||||
"safe-decode-uri-component": "^1.2.1",
|
"safe-decode-uri-component": "^1.2.1",
|
||||||
"tunnel": "^0.0.6",
|
"tunnel": "^0.0.6",
|
||||||
"xml2js": "^0.6.2",
|
"xml2js": "^0.6.2",
|
||||||
"yargs": "^18.0.0"
|
"yargs": "^18.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3.3.5",
|
"@eslint/eslintrc": "^3.3.7",
|
||||||
"@eslint/js": "^9.39.4",
|
"@eslint/js": "^9.39.5",
|
||||||
"@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.5.0",
|
"@types/node": "25.9.2",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.60.0",
|
"@typescript-eslint/eslint-plugin": "^8.71.0",
|
||||||
"@typescript-eslint/parser": "^8.60.0",
|
"@typescript-eslint/parser": "^8.71.0",
|
||||||
"eslint": "^9.39.4",
|
"eslint": "^9.39.5",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"eslint-plugin-html": "^8.1.4",
|
"eslint-plugin-html": "^8.2.1",
|
||||||
"eslint-plugin-prettier": "^5.5.6",
|
"eslint-plugin-prettier": "^5.5.6",
|
||||||
"globals": "^17.6.0",
|
"globals": "^17.13.0",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"intelli-espower-loader": "^1.1.0",
|
"intelli-espower-loader": "^1.1.0",
|
||||||
"lint-staged": "^16.4.0",
|
"lint-staged": "^16.4.0",
|
||||||
"mocha": "^11.7.6",
|
"mocha": "^11.8.0",
|
||||||
"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.3",
|
"prettier": "^3.9.9",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "^5.9.3"
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public",
|
||||||
|
"registry": "https://registry.npmjs.org/"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+722
-374
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,9 @@
|
|||||||
|
packages:
|
||||||
|
- '.'
|
||||||
|
|
||||||
|
allowBuilds:
|
||||||
|
core-js: true
|
||||||
|
es5-ext: true
|
||||||
|
|
||||||
|
minimumReleaseAgeExclude:
|
||||||
|
- '@neteasecloudmusicapienhanced/[email protected]'
|
||||||
+495
-44
@@ -115,6 +115,182 @@
|
|||||||
textarea:focus {
|
textarea:focus {
|
||||||
border-color: #333;
|
border-color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===== 自定义选项区域 ===== */
|
||||||
|
.section-container {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
background: #fafafa;
|
||||||
|
border: 1px solid #e8e8e8;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-container + .section-container {
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #555;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title .count-badge {
|
||||||
|
font-size: 11px;
|
||||||
|
background: #ddd;
|
||||||
|
color: #666;
|
||||||
|
padding: 1px 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dynamic-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dynamic-row .key-input {
|
||||||
|
flex: 0 0 200px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
outline: none;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dynamic-row .value-input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dynamic-row .key-input:focus,
|
||||||
|
.dynamic-row .value-input:focus {
|
||||||
|
border-color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-btn {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
padding: 0;
|
||||||
|
background: #fff;
|
||||||
|
color: #999;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.2s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-btn:hover {
|
||||||
|
background: #fee;
|
||||||
|
color: #e33;
|
||||||
|
border-color: #fcc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-btn {
|
||||||
|
background: #fff;
|
||||||
|
color: #333;
|
||||||
|
border: 1px dashed #ccc;
|
||||||
|
padding: 6px 16px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-btn:hover {
|
||||||
|
border-color: #999;
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 参数编辑区 Tab ===== */
|
||||||
|
.param-container {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-header-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 0;
|
||||||
|
background: #f0f0f0;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-btn {
|
||||||
|
padding: 4px 14px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: #888;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-btn:hover {
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-btn.active {
|
||||||
|
background: #fff;
|
||||||
|
color: #333;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-panel {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-panel textarea {
|
||||||
|
flex: 1;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 350px;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-panel textarea:focus {
|
||||||
|
border-color: #333;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -125,6 +301,16 @@
|
|||||||
<label for="uri">URI</label>
|
<label for="uri">URI</label>
|
||||||
<input type="text" id="uri" name="uri" value="/api/song/lyric/v1">
|
<input type="text" id="uri" name="uri" value="/api/song/lyric/v1">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="method">请求方法</label>
|
||||||
|
<select id="method" name="method">
|
||||||
|
<option value="POST" selected>POST</option>
|
||||||
|
<option value="GET">GET</option>
|
||||||
|
<option value="PUT">PUT</option>
|
||||||
|
<option value="DELETE">DELETE</option>
|
||||||
|
<option value="PATCH">PATCH</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label for="crypto">加密方式</label>
|
<label for="crypto">加密方式</label>
|
||||||
<select id="crypto" name="crypto">
|
<select id="crypto" name="crypto">
|
||||||
@@ -133,22 +319,45 @@
|
|||||||
<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="xeapi">xeapi</option>
|
||||||
|
<option value="neapi">neapi</option>
|
||||||
<option value="" selected>(默认)</option>
|
<option value="" selected>(默认)</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="timeout">超时(ms)</label>
|
||||||
|
<input type="number" id="timeout" name="timeout" value="30000" min="0" step="1000" placeholder="0 表示无超时">
|
||||||
|
</div>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label></label>
|
<label></label>
|
||||||
<button type="submit">发送请求</button>
|
<button type="submit">发送请求</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<!-- ===== 自定义请求头 ===== -->
|
||||||
|
<div class="section-container" id="headersSection">
|
||||||
|
<div class="section-title">
|
||||||
|
请求头 (Headers)
|
||||||
|
<span class="count-badge" id="headerCount">4</span>
|
||||||
|
</div>
|
||||||
|
<div id="headersList"></div>
|
||||||
|
<button class="add-btn" onclick="addHeaderRow('', '')">+ 添加请求头</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="data-result">
|
<div class="data-result">
|
||||||
<div>
|
<div>
|
||||||
<label for="result">响应结果</label>
|
<label for="result">响应结果</label>
|
||||||
<textarea id="result" name="result" readonly></textarea>
|
<textarea id="result" name="result" readonly></textarea>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="param-container">
|
||||||
<label for="data">请求数据</label>
|
<div class="param-header">
|
||||||
<textarea id="data" name="data">
|
<span class="param-header-title">请求参数</span>
|
||||||
|
<div class="param-tabs">
|
||||||
|
<button class="tab-btn active" data-mode="json" onclick="switchParamTab('json')">JSON</button>
|
||||||
|
<button class="tab-btn" data-mode="kv" onclick="switchParamTab('kv')">键值对</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="param-panel" id="jsonPanel">
|
||||||
|
<textarea id="data" name="data">
|
||||||
{
|
{
|
||||||
"cp": false,
|
"cp": false,
|
||||||
"id": "2058263032",
|
"id": "2058263032",
|
||||||
@@ -160,64 +369,306 @@
|
|||||||
"ytv": 0,
|
"ytv": 0,
|
||||||
"yv": 0
|
"yv": 0
|
||||||
}</textarea>
|
}</textarea>
|
||||||
|
</div>
|
||||||
|
<div class="param-panel" id="kvPanel" style="display:none">
|
||||||
|
<div id="kvList"></div>
|
||||||
|
<button class="add-btn" onclick="addKvRow('', '')">+ 添加参数</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="https://fastly.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
<script src="https://fastly.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
async function sendRequest() {
|
// ===== 动态行管理 =====
|
||||||
document.getElementById('result').value = ""
|
function escapeHtml(str) {
|
||||||
|
if (!str) return '';
|
||||||
|
return String(str)
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
}
|
||||||
|
|
||||||
const uri = document.getElementById('uri').value;
|
function addHeaderRow(key, value) {
|
||||||
const crypto = document.getElementById('crypto').value;
|
const list = document.getElementById('headersList');
|
||||||
const data = document.getElementById('data').value;
|
const row = document.createElement('div');
|
||||||
|
row.className = 'dynamic-row';
|
||||||
|
row.innerHTML =
|
||||||
|
'<input type="text" class="key-input" placeholder="Header 名称" value="' +
|
||||||
|
escapeHtml(key) +
|
||||||
|
'">' +
|
||||||
|
'<input type="text" class="value-input" placeholder="值" value="' +
|
||||||
|
escapeHtml(value) +
|
||||||
|
'">' +
|
||||||
|
'<button class="remove-btn" onclick="removeRow(this)" title="删除">✕</button>';
|
||||||
|
list.appendChild(row);
|
||||||
|
updateCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
function addKvRow(key, value) {
|
||||||
|
const list = document.getElementById('kvList');
|
||||||
|
const row = document.createElement('div');
|
||||||
|
row.className = 'dynamic-row';
|
||||||
|
row.innerHTML =
|
||||||
|
'<input type="text" class="key-input" placeholder="参数名" value="' +
|
||||||
|
escapeHtml(key) +
|
||||||
|
'">' +
|
||||||
|
'<input type="text" class="value-input" placeholder="值" value="' +
|
||||||
|
escapeHtml(value) +
|
||||||
|
'">' +
|
||||||
|
'<button class="remove-btn" onclick="removeRow(this)" title="删除">✕</button>';
|
||||||
|
list.appendChild(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeRow(btn) {
|
||||||
|
btn.parentElement.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectHeaders() {
|
||||||
|
const headers = {};
|
||||||
|
document.querySelectorAll('#headersList .dynamic-row').forEach(function (row) {
|
||||||
|
var key = row.querySelector('.key-input').value.trim();
|
||||||
|
var value = row.querySelector('.value-input').value;
|
||||||
|
if (key) headers[key] = value;
|
||||||
|
});
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectKvData() {
|
||||||
|
var obj = {};
|
||||||
|
document.querySelectorAll('#kvList .dynamic-row').forEach(function (row) {
|
||||||
|
var key = row.querySelector('.key-input').value.trim();
|
||||||
|
var val = row.querySelector('.value-input').value;
|
||||||
|
if (!key) return;
|
||||||
|
// 自动类型转换
|
||||||
|
if (val === 'true') obj[key] = true;
|
||||||
|
else if (val === 'false') obj[key] = false;
|
||||||
|
else if (val === 'null') obj[key] = null;
|
||||||
|
else if (/^-?\d+(\.\d+)?$/.test(val) && !isNaN(Number(val))) obj[key] = Number(val);
|
||||||
|
else obj[key] = val;
|
||||||
|
});
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Tab 切换 =====
|
||||||
|
function syncJsonToKv() {
|
||||||
try {
|
try {
|
||||||
const res = await axios({
|
var data = JSON.parse(document.getElementById('data').value);
|
||||||
url: `/api?timestamp=${Date.now()}`,
|
var list = document.getElementById('kvList');
|
||||||
method: 'post',
|
list.innerHTML = '';
|
||||||
data: {
|
Object.keys(data).forEach(function (k) {
|
||||||
uri: uri,
|
addKvRow(k, String(data[k]));
|
||||||
data: JSON.parse(data),
|
|
||||||
crypto: crypto,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
return true;
|
||||||
document.getElementById('result').value = JSON.stringify(res.data, null, 2);
|
} catch (e) {
|
||||||
} catch (error) {
|
return false;
|
||||||
document.getElementById('result').value = 'Request failed: ' + error.message;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
(function fillFromQuery() {
|
function syncKvToJson() {
|
||||||
const params = new URLSearchParams(window.location.search);
|
var obj = collectKvData();
|
||||||
if (!params.toString()) return;
|
document.getElementById('data').value = JSON.stringify(obj, null, 2);
|
||||||
const uri = params.get('uri');
|
}
|
||||||
const crypto = params.get('crypto');
|
|
||||||
const dataParam = params.get('data');
|
function switchParamTab(mode) {
|
||||||
if (uri) {
|
var jsonPanel = document.getElementById('jsonPanel');
|
||||||
document.getElementById('uri').value = uri;
|
var kvPanel = document.getElementById('kvPanel');
|
||||||
|
|
||||||
|
if (mode === 'kv') {
|
||||||
|
if (!syncJsonToKv()) {
|
||||||
|
alert('JSON 格式有误,无法切换到键值对模式,请先修正 JSON');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
jsonPanel.style.display = 'none';
|
||||||
|
kvPanel.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
syncKvToJson();
|
||||||
|
kvPanel.style.display = 'none';
|
||||||
|
jsonPanel.style.display = 'block';
|
||||||
}
|
}
|
||||||
if (crypto) {
|
|
||||||
const cryptoSelect = document.getElementById('crypto');
|
document.querySelectorAll('.tab-btn').forEach(function (btn) {
|
||||||
if ([...cryptoSelect.options].some((opt) => opt.value === crypto)) {
|
btn.classList.toggle('active', btn.dataset.mode === mode);
|
||||||
cryptoSelect.value = crypto;
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== 默认请求头(直接套用后端预设值)=====
|
||||||
|
var defaultHeaders = [
|
||||||
|
{ key: 'Cookie', value: '' },
|
||||||
|
{
|
||||||
|
key: 'User-Agent',
|
||||||
|
value: '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 Edg/124.0.0.0',
|
||||||
|
},
|
||||||
|
{ key: 'Referer', value: 'https://music.163.com' },
|
||||||
|
{ key: 'Content-Type', value: 'application/json' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// ===== 发送请求 =====
|
||||||
|
async function sendRequest() {
|
||||||
|
document.getElementById('result').value = '';
|
||||||
|
|
||||||
|
var uri = document.getElementById('uri').value;
|
||||||
|
var method = document.getElementById('method').value;
|
||||||
|
var crypto = document.getElementById('crypto').value;
|
||||||
|
var timeout = parseInt(document.getElementById('timeout').value, 10) || 0;
|
||||||
|
var headers = collectHeaders();
|
||||||
|
|
||||||
|
// 根据当前激活的 tab 读取数据
|
||||||
|
var parsedData;
|
||||||
|
var activeTab = document.querySelector('.tab-btn.active');
|
||||||
|
if (activeTab && activeTab.dataset.mode === 'kv') {
|
||||||
|
parsedData = collectKvData();
|
||||||
|
} else {
|
||||||
|
var rawData = document.getElementById('data').value;
|
||||||
|
try {
|
||||||
|
parsedData = rawData.trim() ? JSON.parse(rawData) : {};
|
||||||
|
} catch (e) {
|
||||||
|
document.getElementById('result').value =
|
||||||
|
'请求数据格式错误:' + e.message;
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (dataParam) {
|
|
||||||
const decoded = dataParam;
|
var axiosConfig = {
|
||||||
try {
|
url: '/api?timestamp=' + Date.now(),
|
||||||
document.getElementById('data').value = JSON.stringify(
|
method: method,
|
||||||
JSON.parse(decoded),
|
};
|
||||||
null,
|
|
||||||
2,
|
if (timeout > 0) {
|
||||||
|
axiosConfig.timeout = timeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (method === 'GET') {
|
||||||
|
axiosConfig.params = {
|
||||||
|
uri: uri,
|
||||||
|
crypto: crypto,
|
||||||
|
data: JSON.stringify(parsedData),
|
||||||
|
headers: Object.keys(headers).length ? JSON.stringify(headers) : undefined,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
axiosConfig.data = {
|
||||||
|
uri: uri,
|
||||||
|
crypto: crypto,
|
||||||
|
data: parsedData,
|
||||||
|
headers: Object.keys(headers).length ? headers : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
var res = await axios(axiosConfig);
|
||||||
|
document.getElementById('result').value = JSON.stringify(
|
||||||
|
res.data,
|
||||||
|
null,
|
||||||
|
2,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
document.getElementById('data').value = decoded;
|
document.getElementById('result').value =
|
||||||
}
|
'Request failed: ' + error.message;
|
||||||
}
|
}
|
||||||
})();
|
}
|
||||||
|
|
||||||
|
// ===== 从 URL 查询参数恢复 =====
|
||||||
|
(function fillFromQuery() {
|
||||||
|
var params = new URLSearchParams(window.location.search);
|
||||||
|
if (!params.toString()) {
|
||||||
|
// 没有查询参数,用默认 header
|
||||||
|
defaultHeaders.forEach(function (h) {
|
||||||
|
addHeaderRow(h.key, h.value);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var uri = params.get('uri');
|
||||||
|
var crypto = params.get('crypto');
|
||||||
|
var dataParam = params.get('data');
|
||||||
|
var method = params.get('method');
|
||||||
|
var timeout = params.get('timeout');
|
||||||
|
var headersParam = params.get('headers');
|
||||||
|
var kvParams = params.get('kvParams');
|
||||||
|
var paramMode = params.get('paramMode');
|
||||||
|
|
||||||
|
if (uri) document.getElementById('uri').value = uri;
|
||||||
|
if (method) {
|
||||||
|
var methodSelect = document.getElementById('method');
|
||||||
|
if (
|
||||||
|
[].slice.call(methodSelect.options).some(function (opt) {
|
||||||
|
return opt.value === method;
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
methodSelect.value = method;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (timeout) document.getElementById('timeout').value = timeout;
|
||||||
|
|
||||||
|
if (crypto) {
|
||||||
|
var cryptoSelect = document.getElementById('crypto');
|
||||||
|
if (
|
||||||
|
[].slice.call(cryptoSelect.options).some(function (opt) {
|
||||||
|
return opt.value === crypto;
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
cryptoSelect.value = crypto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dataParam) {
|
||||||
|
try {
|
||||||
|
document.getElementById('data').value = JSON.stringify(
|
||||||
|
JSON.parse(dataParam),
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
);
|
||||||
|
} catch (_) {
|
||||||
|
document.getElementById('data').value = dataParam;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (headersParam) {
|
||||||
|
try {
|
||||||
|
var parsed = JSON.parse(headersParam);
|
||||||
|
Object.keys(parsed).forEach(function (k) {
|
||||||
|
addHeaderRow(k, parsed[k]);
|
||||||
|
});
|
||||||
|
} catch (_) {
|
||||||
|
// fallback: 用默认 header
|
||||||
|
defaultHeaders.forEach(function (h) {
|
||||||
|
addHeaderRow(h.key, h.value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
defaultHeaders.forEach(function (h) {
|
||||||
|
addHeaderRow(h.key, h.value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kvParams) {
|
||||||
|
try {
|
||||||
|
var parsed = JSON.parse(kvParams);
|
||||||
|
// 先填充 kv 列表,再切换到 kv 模式
|
||||||
|
Object.keys(parsed).forEach(function (k) {
|
||||||
|
addKvRow(k, String(parsed[k]));
|
||||||
|
});
|
||||||
|
if (paramMode === 'kv') {
|
||||||
|
switchParamTab('kv');
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
} else if (paramMode === 'kv' && dataParam) {
|
||||||
|
// 从 data 参数同步到键值对模式
|
||||||
|
try {
|
||||||
|
var data = JSON.parse(dataParam);
|
||||||
|
Object.keys(data).forEach(function (k) {
|
||||||
|
addKvRow(k, String(data[k]));
|
||||||
|
});
|
||||||
|
switchParamTab('kv');
|
||||||
|
} catch (_) {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -255,9 +255,13 @@
|
|||||||
}
|
}
|
||||||
logs.write(`[index] 查询完成。结果数量=${resp.data.result.length}`)
|
logs.write(`[index] 查询完成。结果数量=${resp.data.result.length}`)
|
||||||
for (var song of resp.data.result) {
|
for (var song of resp.data.result) {
|
||||||
logs.write(
|
var a = document.createElement('a');
|
||||||
`[result] <a target="_blank" href="https://music.163.com/song?id=${song.song.id}">${song.song.name} - ${song.song.album.name} (${song.startTime / 1000}s)</a>`
|
a.href = 'https://music.163.com/song?id=' + encodeURIComponent(song.song.id);
|
||||||
)
|
a.target = '_blank';
|
||||||
|
a.textContent = song.song.name + ' - ' + song.song.album.name + ' (' + (song.startTime / 1000) + 's)';
|
||||||
|
logs.appendChild(document.createTextNode('[result] '));
|
||||||
|
logs.appendChild(a);
|
||||||
|
logs.appendChild(document.createElement('br'));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
# 网易云音乐 API Enhanced
|
# NeteaseCloudMusicAPI Enhanced
|
||||||
|
|
||||||
> 🔍 网易云音乐API Node.js服务的复兴项目
|
> 🎉 全网收集最全的网易云音乐api接口 基于[NeteaseCloudMusicAPI](https://github.com/binaryify/NeteaseCloudMusicApi)的复刻版本
|
||||||
|
|
||||||
- 基于原版网易云API新增更多有趣的功能
|
- ⚡ 四种加密模式 · 后端代理
|
||||||
- 具备登录接口,多达200多个接口
|
- 🪛 具备多达200多个接口
|
||||||
- 更完善的文档
|
- 📄 更完善的文档
|
||||||
|
|
||||||
|
|
||||||
[Github](https://github.com/neteasecloudmusicapienhanced/api-enhanced)
|
[Github](https://github.com/neteasecloudmusicapienhanced/api-enhanced)
|
||||||
[Get Started](#neteasecloudmusicapienhanced)
|
[前往本家](https://github.com/binaryify/NeteaseCloudMusicApi)
|
||||||
|
[快速开始](#neteasecloudmusicapienhanced)
|
||||||
|
|
||||||

|

|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 2.4 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 14 KiB |
+651
-23
@@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
网易云音乐 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)
|
||||||
@@ -90,6 +88,7 @@ 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 部署方法
|
||||||
@@ -103,6 +102,7 @@ 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,6 +115,7 @@ export PORT=9000
|
|||||||
- 腾讯云 serverless 并不是免费的,前三个月有免费额度,之后收费
|
- 腾讯云 serverless 并不是免费的,前三个月有免费额度,之后收费
|
||||||
- 当前(2024-08-24), 用此法创建的话, 会`默认`关联一个"日志服务-日志主题"(创建过程中没有提醒), 此服务是计量收费的
|
- 当前(2024-08-24), 用此法创建的话, 会`默认`关联一个"日志服务-日志主题"(创建过程中没有提醒), 此服务是计量收费的
|
||||||
|
|
||||||
|
|
||||||
## 可以使用代理
|
## 可以使用代理
|
||||||
|
|
||||||
在 query 参数中加上 proxy=your-proxy 即可让这一次的请求使用 proxy
|
在 query 参数中加上 proxy=your-proxy 即可让这一次的请求使用 proxy
|
||||||
@@ -182,6 +183,7 @@ 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
|
||||||
|
|
||||||
@@ -214,12 +216,17 @@ $ sudo docker run -d -p 3000:3000 netease-music-api
|
|||||||
- 大部分请求参数或返回内容可在 `/api_decrypt.html` 里解析
|
- 大部分请求参数或返回内容可在 `/api_decrypt.html` 里解析
|
||||||
- 请求参数模式下, 解密结果可直接带到 `/api.html` 继续调试
|
- 请求参数模式下, 解密结果可直接带到 `/api.html` 继续调试
|
||||||
- 需要返回值加密时, 可传 `e_r=1`, `weapi` 和 `eapi` 都支持
|
- 需要返回值加密时, 可传 `e_r=1`, `weapi` 和 `eapi` 都支持
|
||||||
- 目前支持算法 有 `weapi`, `eapi`, `linuxapi` 和 `xeapi` (xeapi 是一种不加密的特殊算法, 主要用于调试加密前的原始请求参数)
|
- 目前支持算法 有 `weapi`, `eapi`, `linuxapi`, `xeapi` 和 `neapi` (xeapi 是一种不加密的特殊算法, 主要用于调试加密前的原始请求参数; neapi 走独立的压缩加密通道, 配置由 `/register/neapikey` 自动获取与刷新)
|
||||||
|
|
||||||
|
|
||||||
## 接口文档
|
## 接口文档
|
||||||
|
|
||||||
### 调用前须知
|
### 调用前须知
|
||||||
|
|
||||||
|
AI 生成的图,仅供娱乐()
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
!> 本项目不提供线上 demo, 只提供在线文档服务, 请不要轻易信任使用他人提供的公开服务,以免发生安全问题,泄露自己的账号和密码
|
!> 本项目不提供线上 demo, 只提供在线文档服务, 请不要轻易信任使用他人提供的公开服务,以免发生安全问题,泄露自己的账号和密码
|
||||||
|
|
||||||
!> 为使用方便,降低门槛, 文档示例接口直接使用了 GET 请求,本项目同时支持 GET/POST 请按实际需求使用 (POST 请求 url 必须添加时间戳,使每次请求 url 不一样,不然请求会被缓存)
|
!> 为使用方便,降低门槛, 文档示例接口直接使用了 GET 请求,本项目同时支持 GET/POST 请按实际需求使用 (POST 请求 url 必须添加时间戳,使每次请求 url 不一样,不然请求会被缓存)
|
||||||
@@ -276,7 +283,9 @@ $ sudo docker run -d -p 3000:3000 netease-music-api
|
|||||||
|
|
||||||
`md5_password`: md5 加密后的密码,传入后 `password` 参数将失效
|
`md5_password`: md5 加密后的密码,传入后 `password` 参数将失效
|
||||||
|
|
||||||
`captcha`: 验证码,使用 [`/captcha/sent`](#发送验证码)接口传入手机号获取验证码,调用此接口传入验证码,可使用验证码登录,传入后 `password` 参数将失效
|
`captcha`: 验证码,使用 `/captcha/sent` 或 `/captcha/sent/v1`接口传入手机号获取验证码,调用此接口传入验证码,可使用验证码登录,传入后 `password` 参数将失效
|
||||||
|
|
||||||
|
`sca`: 网易易盾滑块验证token, 获取方式未知
|
||||||
|
|
||||||
**接口地址 :** `/login/cellphone`
|
**接口地址 :** `/login/cellphone`
|
||||||
|
|
||||||
@@ -390,6 +399,19 @@ body {
|
|||||||
|
|
||||||
**调用例子 :** `/captcha/sent?phone=13xxx`
|
**调用例子 :** `/captcha/sent?phone=13xxx`
|
||||||
|
|
||||||
|
### 新版发送验证码
|
||||||
|
|
||||||
|
说明 : 调用此接口 ,传入手机号码, 可发送验证码
|
||||||
|
|
||||||
|
**必选参数 :** `phone`: 手机号码
|
||||||
|
|
||||||
|
**可选参数 :**
|
||||||
|
`ctcode`: 国家区号,默认 86 即中国
|
||||||
|
|
||||||
|
**接口地址 :** `/captcha/sent/v1`
|
||||||
|
|
||||||
|
**调用例子 :** `/captcha/sent/v1?phone=13xxx`
|
||||||
|
|
||||||
### 验证验证码
|
### 验证验证码
|
||||||
|
|
||||||
说明 : 调用此接口 ,传入手机号码和验证码, 可校验验证码是否正确
|
说明 : 调用此接口 ,传入手机号码和验证码, 可校验验证码是否正确
|
||||||
@@ -804,6 +826,8 @@ tags: 歌单标签
|
|||||||
|
|
||||||
`lasttime` : 返回数据的 `lasttime` ,默认-1,传入上一次返回结果的 lasttime,将会返回下一页的数据
|
`lasttime` : 返回数据的 `lasttime` ,默认-1,传入上一次返回结果的 lasttime,将会返回下一页的数据
|
||||||
|
|
||||||
|
接口会传入官方客户端使用的 `fromRN=true`,登录用户读取自己的动态时可获得网易云允许本人查看的非公开动态。返回的 `size` 是网易云账户侧统计值,不保证等于分页后实际可获取的动态数量。
|
||||||
|
|
||||||
**接口地址 :** `/user/event`
|
**接口地址 :** `/user/event`
|
||||||
|
|
||||||
**调用例子 :** `/user/event?uid=32953014` `/user/event?uid=32953014&limit=1&lasttime=1558011138743`
|
**调用例子 :** `/user/event?uid=32953014` `/user/event?uid=32953014&limit=1&lasttime=1558011138743`
|
||||||
@@ -821,6 +845,38 @@ tags: 歌单标签
|
|||||||
41、21 分享视频
|
41、21 分享视频
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 获取当前登录用户的全部可枚举动态
|
||||||
|
|
||||||
|
说明 : 登录后调用此接口,会组合 `/user/account` 与 `/user/event` 的原子能力:先从 Cookie 取得当前用户 id,再自动跟随 `lasttime` 游标读取至 `more=false`。接口返回网易云允许当前用户本人查看的公开及非公开动态,不能读取其他用户的私密动态。
|
||||||
|
|
||||||
|
本接口不接受 `limit` 或 `lasttime`,一次请求会完成全部上游分页。账号动态较多时,请预留足够的请求时间。
|
||||||
|
|
||||||
|
数量字段说明:
|
||||||
|
|
||||||
|
- `size`:网易云返回的账户统计数量,可能包含已删除、被屏蔽、资源失效或旧类型且不再下发的记录。
|
||||||
|
- `retrievedCount`:本次实际取得的唯一动态数量,始终等于 `events.length`。
|
||||||
|
- `unavailableCount`:`size - retrievedCount` 的正差值;缺失记录没有返回 id,无法继续读取或修改。
|
||||||
|
- `sizeMismatch`:`size` 与 `retrievedCount` 是否不一致。若上游未返回 `size`,相关字段为 `null`。
|
||||||
|
- `pageCount`:本次实际请求的上游分页数量。
|
||||||
|
|
||||||
|
每条动态的 `privacySetting` 表示当前可见权限:`0` 为所有人,`1` 为我关注的人,`2` 为仅自己,`6` 为互相关注的人。
|
||||||
|
|
||||||
|
**接口地址 :** `/user/event/all`
|
||||||
|
|
||||||
|
**调用例子 :** `/user/event/all`
|
||||||
|
|
||||||
|
### 修改动态可见权限
|
||||||
|
|
||||||
|
说明 : 登录后调用此接口,可以修改当前账号本人发布的单条动态的可见权限。此接口只负责一次原子修改;上游没有批量修改接口。如需批量操作,可先调用 `/user/event/all` 并按 `privacySetting` 筛选,再由调用方逐条调用本接口,同时自行处理限速、失败重试和部分成功。
|
||||||
|
|
||||||
|
**必选参数 :** `evId` : 动态 id
|
||||||
|
|
||||||
|
`privacy` : 目标可见权限。`0` 为所有人,`1` 为我关注的人,`2` 为仅自己,`6` 为互相关注的人
|
||||||
|
|
||||||
|
**接口地址 :** `/event/privacy`
|
||||||
|
|
||||||
|
**调用例子 :** `/event/privacy?evId=6712917601&privacy=0`
|
||||||
|
|
||||||
### 转发用户动态
|
### 转发用户动态
|
||||||
|
|
||||||
说明 : 登录后调用此接口 ,可以转发用户动态
|
说明 : 登录后调用此接口 ,可以转发用户动态
|
||||||
@@ -1248,12 +1304,14 @@ tags: 歌单标签
|
|||||||
|
|
||||||
**必选参数 :** `id` : 音乐 id
|
**必选参数 :** `id` : 音乐 id
|
||||||
`level`: 播放音质等级, 分为 `standard` => `标准`,`higher` => `较高`, `exhigh`=>`极高`,
|
`level`: 播放音质等级, 分为 `standard` => `标准`,`higher` => `较高`, `exhigh`=>`极高`,
|
||||||
`lossless`=>`无损`, `hires`=>`Hi-Res`, `jyeffect` => `高清环绕声`, `sky` => `沉浸环绕声`, `dolby` => `杜比全景声`, `jymaster` => `超清母带`
|
`lossless`=>`无损`, `hires`=>`Hi-Res`, `jyeffect` => `高清臻音`, `dolby` => `杜比全景声`, `vivid` => `臻音全景声`, `jymaster` => `超清母带`, `sky` => `沉浸环绕声`
|
||||||
`unblock`: 是否使用使用歌曲解锁, 分为`true`和`false`
|
`unblock`: 是否使用使用歌曲解锁, 分为`true`和`false`
|
||||||
|
|
||||||
|
**可选参数 :** `immerseType`: 沉浸声环绕声类型, 分为`c512` => `新版c51类型`, `ste2` => `新版环绕立体声类型`, `aac2` => `新版aac类型`, `c51` => `c51类型`, `ste` => `环绕立体声类型`, `aac` => `aac类型`, 仅在 `level=sky` 时生效, 默认为 `c51`
|
||||||
|
|
||||||
**接口地址 :** `/song/url/v1`
|
**接口地址 :** `/song/url/v1`
|
||||||
|
|
||||||
**调用例子 :** `/song/url/v1?id=1969519579&level=exhigh` `/song/url/v1?id=1969519579,33894312&level=lossless`
|
**调用例子 :** `/song/url/v1?id=1969519579&level=exhigh` `/song/url/v1?id=1969519579,33894312&level=lossless` `/song/url/v1?id=1969519579&level=sky&immerseType=ste` `/song/url/v1?id=1969519579&level=vivid`
|
||||||
|
|
||||||
说明:`杜比全景声`音质需要设备支持,不同的设备可能会返回不同码率的 url。cookie 需要传入`os=pc`保证返回正常码率的 url。
|
说明:`杜比全景声`音质需要设备支持,不同的设备可能会返回不同码率的 url。cookie 需要传入`os=pc`保证返回正常码率的 url。
|
||||||
|
|
||||||
@@ -1263,7 +1321,7 @@ tags: 歌单标签
|
|||||||
|
|
||||||
**必选参数 :** `id` : 音乐 id
|
**必选参数 :** `id` : 音乐 id
|
||||||
`level`: 播放音质等级, 分为 `standard` => `标准`,`higher` => `较高`, `exhigh`=>`极高`,
|
`level`: 播放音质等级, 分为 `standard` => `标准`,`higher` => `较高`, `exhigh`=>`极高`,
|
||||||
`lossless`=>`无损`, `hires`=>`Hi-Res`, `jyeffect` => `高清环绕声`, `sky` => `沉浸环绕声`, `dolby` => `杜比全景声`, `jymaster` => `超清母带`
|
`lossless`=>`无损`, `hires`=>`Hi-Res`, `jyeffect` => `高清臻音`, `dolby` => `杜比全景声`, `vivid` => `臻音全景声`, `jymaster` => `超清母带`, `sky` => `沉浸环绕声`
|
||||||
`unblock`: 是否使用使用歌曲解锁, 分为`true`和`false`
|
`unblock`: 是否使用使用歌曲解锁, 分为`true`和`false`
|
||||||
|
|
||||||
**接口地址 :** `/song/url/v1/302`
|
**接口地址 :** `/song/url/v1/302`
|
||||||
@@ -1387,8 +1445,6 @@ tags: 歌单标签
|
|||||||
|
|
||||||
说明 : 调用此接口 , 传入类型和歌单 id 可收藏歌单或者取消收藏歌单
|
说明 : 调用此接口 , 传入类型和歌单 id 可收藏歌单或者取消收藏歌单
|
||||||
|
|
||||||
!> 警告: 在`v4.29.7`版本后, 在网易云登陆后请求要带上`timestamp`字段, 否则会导致请求不合法
|
|
||||||
|
|
||||||
**必选参数 :**
|
**必选参数 :**
|
||||||
|
|
||||||
`t` : 类型,1:收藏,2:取消收藏
|
`t` : 类型,1:收藏,2:取消收藏
|
||||||
@@ -1494,6 +1550,7 @@ 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"}]}
|
||||||
@@ -1510,6 +1567,7 @@ 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 (...)
|
||||||
@@ -2333,6 +2391,16 @@ privilege:权限相关信息
|
|||||||
|
|
||||||
**调用例子 :** `/recommend/songs`
|
**调用例子 :** `/recommend/songs`
|
||||||
|
|
||||||
|
### 获取每日推荐歌曲 - v1
|
||||||
|
|
||||||
|
说明 : 调用此接口 , 可获得每日推荐歌曲 ( 需要登录 )
|
||||||
|
|
||||||
|
**可选参数 :** `ispush`: 是否刷新日推 , 默认为 false
|
||||||
|
|
||||||
|
**接口地址 :** `/recommend/songs/v1`
|
||||||
|
|
||||||
|
**调用例子 :** `/recommend/songs/v1`
|
||||||
|
|
||||||
### 每日推荐歌曲-不感兴趣
|
### 每日推荐歌曲-不感兴趣
|
||||||
|
|
||||||
说明 : 日推歌曲标记为不感兴趣( 同时会返回一个新推荐歌曲, 需要登录 )
|
说明 : 日推歌曲标记为不感兴趣( 同时会返回一个新推荐歌曲, 需要登录 )
|
||||||
@@ -2415,6 +2483,18 @@ privilege:权限相关信息
|
|||||||
|
|
||||||
喜欢成功则返回数据的 code 为 200, 其余为失败
|
喜欢成功则返回数据的 code 为 200, 其余为失败
|
||||||
|
|
||||||
|
### 喜欢音乐 - 新版
|
||||||
|
|
||||||
|
说明: 调用此接口 , 传入音乐 id, 可喜欢该音乐
|
||||||
|
|
||||||
|
**必选参数 :** `id`: 歌曲 id
|
||||||
|
|
||||||
|
**可选参数 :** `like`: 布尔值 , 默认为 true 即喜欢 , 若传 false, 则取消喜欢
|
||||||
|
|
||||||
|
**接口地址 :** `/like/v1`
|
||||||
|
|
||||||
|
**调用例子 :** `/like/v1?id=347230`
|
||||||
|
|
||||||
### 喜欢音乐列表
|
### 喜欢音乐列表
|
||||||
|
|
||||||
说明 : 调用此接口 , 传入用户 id, 可获取已喜欢音乐 id 列表(id 数组)
|
说明 : 调用此接口 , 传入用户 id, 可获取已喜欢音乐 id 列表(id 数组)
|
||||||
@@ -2490,6 +2570,30 @@ 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`
|
||||||
|
|
||||||
### 热门歌手
|
### 热门歌手
|
||||||
|
|
||||||
说明 : 调用此接口 , 可获取热门歌手数据
|
说明 : 调用此接口 , 可获取热门歌手数据
|
||||||
@@ -2871,6 +2975,7 @@ type : 地区
|
|||||||
- 适合 Vercel、Netlify 等有请求体限制的平台
|
- 适合 Vercel、Netlify 等有请求体限制的平台
|
||||||
- 需要前端配合实现
|
- 需要前端配合实现
|
||||||
|
|
||||||
|
|
||||||
#### 客户端直传相关接口
|
#### 客户端直传相关接口
|
||||||
|
|
||||||
**获取上传凭证**
|
**获取上传凭证**
|
||||||
@@ -2917,6 +3022,7 @@ type : 地区
|
|||||||
- `artist`: 艺术家
|
- `artist`: 艺术家
|
||||||
- `album`: 专辑名
|
- `album`: 专辑名
|
||||||
|
|
||||||
|
|
||||||
#### 客户端直传流程
|
#### 客户端直传流程
|
||||||
|
|
||||||
1. 客户端计算文件 MD5
|
1. 客户端计算文件 MD5
|
||||||
@@ -2924,6 +3030,7 @@ type : 地区
|
|||||||
3. 如果 `needUpload` 为 true,直接 PUT 文件到 `uploadUrl`
|
3. 如果 `needUpload` 为 true,直接 PUT 文件到 `uploadUrl`
|
||||||
4. 调用 `/cloud/upload/complete` 完成导入
|
4. 调用 `/cloud/upload/complete` 完成导入
|
||||||
|
|
||||||
|
|
||||||
### 云盘歌曲信息匹配纠正
|
### 云盘歌曲信息匹配纠正
|
||||||
|
|
||||||
说明 : 登录后调用此接口,可对云盘歌曲信息匹配纠正,如需取消匹配,asid 需要传 0
|
说明 : 登录后调用此接口,可对云盘歌曲信息匹配纠正,如需取消匹配,asid 需要传 0
|
||||||
@@ -3540,6 +3647,36 @@ type='1009' 获取其 id, 如`/search?keywords= 代码时间 &type=1009`
|
|||||||
|
|
||||||
**调用例子 :** `/yunbei/task/finish?userTaskId=5146243240&depositCode=0`
|
**调用例子 :** `/yunbei/task/finish?userTaskId=5146243240&depositCode=0`
|
||||||
|
|
||||||
|
### 云贝广告任务 - 今日任务状态
|
||||||
|
|
||||||
|
说明 :登录后调用此接口可查询云贝广告任务("听歌/看视频得云贝")今日状态。逆向自云贝任务中心 H5 页面(st.music.163.com/yunbei-listen)。返回 `times`(今日已完成次数)、`amount`(今日累计云贝)、`singleAmount`(单次可得云贝)。单日上限 10 次。
|
||||||
|
|
||||||
|
**接口地址 :** `/yunbei/task/list/v1`
|
||||||
|
|
||||||
|
**调用例子 :** `/yunbei/task/list/v1`
|
||||||
|
|
||||||
|
### 云贝广告任务 - 获取推荐歌曲
|
||||||
|
|
||||||
|
说明 :登录后调用此接口可获取云贝广告任务的推荐歌曲列表。返回数组项含 `songId`、`songName`、`artistName`、`albumUrl`、`songChorusStartTime`、`likeFlag`、`alg`(均为 `alg_payrec_yunBei_*`)。
|
||||||
|
|
||||||
|
**可选参数 :** `offset`: 偏移数量,默认为 0
|
||||||
|
|
||||||
|
`limit`: 取出数量,默认为 10(客户端每次固定取 10 首)
|
||||||
|
|
||||||
|
**接口地址 :** `/yunbei/task/recommend/song`
|
||||||
|
|
||||||
|
**调用例子 :** `/yunbei/task/recommend/song` `/yunbei/task/recommend/song?offset=0&limit=10`
|
||||||
|
|
||||||
|
### 云贝广告任务 - 完成任务领取云贝
|
||||||
|
|
||||||
|
说明 :登录后调用此接口可完成任务并领取云贝。实测仅需传 `yunbeiAmount`(单次云贝数,客户端从 `list` 接口的 `singleAmount` 取值,当前为 150)即可成功领取,无需真实听歌/看视频。单日上限 10 次 × 150 = 1500 云贝/天,超限返回 `code:400 "单日完成任务数已达上限"`。建议领取前先调用 `/yunbei/task/list/v1` 查询今日剩余次数。
|
||||||
|
|
||||||
|
**可选参数 :** `yunbeiAmount`: 单次云贝数,默认为 150
|
||||||
|
|
||||||
|
**接口地址 :** `/yunbei/task/finish/v1`
|
||||||
|
|
||||||
|
**调用例子 :** `/yunbei/task/finish/v1?yunbeiAmount=150`
|
||||||
|
|
||||||
### 云贝收入
|
### 云贝收入
|
||||||
|
|
||||||
说明 :登录后调用此接口可获取云贝收入
|
说明 :登录后调用此接口可获取云贝收入
|
||||||
@@ -3575,6 +3712,32 @@ type='1009' 获取其 id, 如`/search?keywords= 代码时间 &type=1009`
|
|||||||
|
|
||||||
**调用例子 :** `/artist/new/song?limit=1` `/artist/new/song?limit=1&before=1602777625000`
|
**调用例子 :** `/artist/new/song?limit=1` `/artist/new/song?limit=1&before=1602777625000`
|
||||||
|
|
||||||
|
### 关注歌手最近新歌 - 播放全部
|
||||||
|
|
||||||
|
说明 : 登录后调用此接口可获取所有关注歌手最近的 50 首新歌
|
||||||
|
|
||||||
|
**接口地址 :** `/artist/new/song/playall`
|
||||||
|
|
||||||
|
**调用例子 :** `/artist/new/song/playall`
|
||||||
|
|
||||||
|
### 关注歌手新作品(歌曲/MV)
|
||||||
|
|
||||||
|
说明 : 登录后调用此接口可分页获取关注歌手的新歌曲和 MV
|
||||||
|
|
||||||
|
**可选参数 :** `limit`: 返回数量,默认为 10
|
||||||
|
|
||||||
|
`startTimestamp`: 时间游标,首次请求默认为当前时间,后续请求传入上一页返回的时间游标
|
||||||
|
|
||||||
|
`before`: `startTimestamp` 的别名
|
||||||
|
|
||||||
|
`sourceType`: 作品来源类型,已确认 `1` 表示新歌,其他取值待确认,默认为 `1`
|
||||||
|
|
||||||
|
`firstRequest`: 是否首次请求,默认为 `true`,翻页时传 `false`
|
||||||
|
|
||||||
|
**接口地址 :** `/artist/new/song/mv/list/v2`
|
||||||
|
|
||||||
|
**调用例子 :** `/artist/new/song/mv/list/v2` `/artist/new/song/mv/list/v2?startTimestamp=1783680576099&sourceType=1&limit=10&firstRequest=false`
|
||||||
|
|
||||||
### 关注歌手新 MV
|
### 关注歌手新 MV
|
||||||
|
|
||||||
说明 :登录后调用此接口可获取关注歌手新 MV
|
说明 :登录后调用此接口可获取关注歌手新 MV
|
||||||
@@ -3705,6 +3868,14 @@ type='1009' 获取其 id, 如`/search?keywords= 代码时间 &type=1009`
|
|||||||
|
|
||||||
**调用例子 :** `/vip/growthpoint/get?ids=7043206830_7` `/vip/growthpoint/get?ids=8613118351_1,8607552957_1`
|
**调用例子 :** `/vip/growthpoint/get?ids=7043206830_7` `/vip/growthpoint/get?ids=8613118351_1,8607552957_1`
|
||||||
|
|
||||||
|
### 一键领取所有 vip 成长值
|
||||||
|
|
||||||
|
说明 : 登录后调用此接口 , 一键领取所有已完成的会员任务成长值奖励
|
||||||
|
|
||||||
|
**接口地址 :** `/vip/growthpoint/getall`
|
||||||
|
|
||||||
|
**调用例子 :** `/vip/growthpoint/getall`
|
||||||
|
|
||||||
### 歌手粉丝
|
### 歌手粉丝
|
||||||
|
|
||||||
说明 : 调用此接口 , 传入歌手 id, 可获取歌手粉丝
|
说明 : 调用此接口 , 传入歌手 id, 可获取歌手粉丝
|
||||||
@@ -3989,6 +4160,16 @@ type='1009' 获取其 id, 如`/search?keywords= 代码时间 &type=1009`
|
|||||||
|
|
||||||
**调用例子:** `/song/wiki/summary?id=1958384591`
|
**调用例子:** `/song/wiki/summary?id=1958384591`
|
||||||
|
|
||||||
|
### 音乐百科
|
||||||
|
|
||||||
|
说明: 调用此接口可以获取歌曲的音乐百科信息
|
||||||
|
|
||||||
|
**接口地址:** `/song/wiki/info`
|
||||||
|
|
||||||
|
**必选参数:** `id`: 歌曲 ID
|
||||||
|
|
||||||
|
**调用例子:** `/song/wiki/info?id=1958384591`
|
||||||
|
|
||||||
### 乐谱列表
|
### 乐谱列表
|
||||||
|
|
||||||
说明: 调用此接口可以获取歌曲的乐谱列表
|
说明: 调用此接口可以获取歌曲的乐谱列表
|
||||||
@@ -4196,6 +4377,7 @@ ONLINE 已发布
|
|||||||
- `voiceFeeType: 0`:返回免费的声音
|
- `voiceFeeType: 0`:返回免费的声音
|
||||||
- `voiceFeeType: 1`:返回收费的声音
|
- `voiceFeeType: 1`:返回收费的声音
|
||||||
|
|
||||||
|
|
||||||
### 播客声音详情
|
### 播客声音详情
|
||||||
|
|
||||||
说明: 获取播客里的声音详情
|
说明: 获取播客里的声音详情
|
||||||
@@ -4250,27 +4432,33 @@ ONLINE 已发布
|
|||||||
|
|
||||||
### 播客上传声音
|
### 播客上传声音
|
||||||
|
|
||||||
说明: 可以上传声音到播客,例子在 `/public/voice_upload.html` 访问地址: <a href="/voice_upload.html" target="_blank">/voice_upload.html</a>
|
说明: 登录后调用此接口,使用`'Content-Type': 'multipart/form-data'`上传声音文件 formData(name 为`songFile`),可通过 formData(name 为`imgFile`)同时上传声音封面。例子在 `/public/voice_upload.html` 访问地址: <a href="/voice_upload.html" target="_blank">/voice_upload.html</a>
|
||||||
|
|
||||||
**接口地址:** `/voice/upload`
|
**接口地址:** `/voice/upload`
|
||||||
|
|
||||||
**必选参数:**
|
**必选参数:**
|
||||||
`voiceListId`: 播客 id
|
|
||||||
|
|
||||||
`coverImgId`: 播客封面
|
`songFile`: 声音文件
|
||||||
|
|
||||||
|
`voiceListId`: 播客 id
|
||||||
|
|
||||||
`categoryId`: 分类 id
|
`categoryId`: 分类 id
|
||||||
|
|
||||||
`secondCategoryId`:次级分类 id
|
`secondCategoryId`: 次级分类 id
|
||||||
|
|
||||||
`description`: 声音介绍
|
`description`: 声音介绍
|
||||||
|
|
||||||
**可选参数:**
|
**可选参数:**
|
||||||
|
|
||||||
|
`imgFile`: 声音封面图片文件,上传后会自动生成图片 id。与`coverImgId`同时传入时,优先使用`imgFile`
|
||||||
|
|
||||||
|
`coverImgId`: 已上传的声音封面图片 id,未传入`imgFile`时使用该值
|
||||||
|
|
||||||
`songName`: 声音名称
|
`songName`: 声音名称
|
||||||
|
|
||||||
`privacy`: 设为隐私声音,播客如果是隐私博客,则必须设为 1
|
`privacy`: 设为隐私声音,播客如果是隐私播客,则必须设为 1
|
||||||
|
|
||||||
`publishTime`:默认立即发布,定时发布的话需传入时间戳
|
`publishTime`: 默认立即发布,定时发布的话需传入时间戳
|
||||||
|
|
||||||
`autoPublish`: 是否发布动态,是则传入 1
|
`autoPublish`: 是否发布动态,是则传入 1
|
||||||
|
|
||||||
@@ -4497,7 +4685,7 @@ qrCodeStatus:20,detailReason:0 验证成功 qrCodeStatus:21,detailReason:0 二
|
|||||||
|
|
||||||
### 歌曲音质详情
|
### 歌曲音质详情
|
||||||
|
|
||||||
说明: 调用此接口获取歌曲各个音质的文件信息,与 `获取歌曲详情` 接口相比,多出 `高清环绕声`、`沉浸环绕声`、`超清母带`等音质的信息
|
说明: 调用此接口获取歌曲各个音质的文件信息,与 `获取歌曲详情` 接口相比,多出`高清臻音`、`杜比全景声`、`臻音全景声`、`超清母带`、`沉浸环绕声`等音质的信息
|
||||||
|
|
||||||
**必选参数:**
|
**必选参数:**
|
||||||
|
|
||||||
@@ -4534,11 +4722,11 @@ qrCodeStatus:20,detailReason:0 验证成功 qrCodeStatus:21,detailReason:0 二
|
|||||||
|
|
||||||
**必选参数:**
|
**必选参数:**
|
||||||
|
|
||||||
`mode`: 模式 (aidj, DEFAULT, FAMILIAR, EXPLORE, SCENE_RCMD)
|
`mode`: 模式 (`DEFAULT`: 默认模式,`FAMILIAR`: 熟悉模式,`EXPLORE`: 探索模式,`SCENE_RCMD`: 场景模式, 需要传入`submode`参数,`PUZZLE_MODE_RCMD`: 拼图模式)
|
||||||
|
|
||||||
**可选参数:**
|
**可选参数:**
|
||||||
|
|
||||||
`submode`: 当 mode 为 SCENE_RCMD 是可为 ( EXERCISE, FOCUS, NIGHT_EMO )
|
`submode`: 当 mode 为 SCENE_RCMD 时传入 ( `EXERCISE`: 运动, `FOCUS`: 专注, `NIGHT_EMO`: 伤感, `SLEEP_HELP`: 助眠, `RELAX`: 放松, `CHEERFUL`: 欢快, `LYRICAL`: 抒情, `CURE`: 治愈, `SWEET`: 情歌, `RHYTHM_BLUES`: R&B, `RAINY`: 雨天, `GAMES`: 游戏, `RAP`: 说唱, `K_POP`: K-Pop, `ORIGINAL_MUSICIAL`: 宝藏原创, `ELECTRONIC`: 电音, `COMMUTE`: 出行, `TAKE_SHOWER`: 洗澡, `COFFEE_SHOP`: 咖啡馆, `ROCK`: 摇滚, `INSPIRATIONAL`: 励志, `CHINESE`: 华语, `ENGLISH`: 欧美, `YUEYU`: 粤语, `MANYAO`: 慢摇DJ, `JINGDIAN`: 经典, `LIGHT`: 轻音乐, `GUOFENG`: 国风, `FOLK`: 民谣, `ACG`: 二次元, `GUDIAN`: 古典, `JAZZ`: 爵士, `JAPANESE`: 日语, `GLOBAL`: 全球, `FRANCH`: 法语, `BLUE`: 蓝调, `DANCE`: 舞蹈, `LATIN`: 拉丁, `PUNK`: 放克, `COUNTRY`: 乡村乐, `MUSICAL`: 音乐剧, `YINGSHI`: 影视)
|
||||||
|
|
||||||
**接口地址:** `/personal/fm/mode`
|
**接口地址:** `/personal/fm/mode`
|
||||||
|
|
||||||
@@ -4618,7 +4806,7 @@ bitrate = Math.floor(br / 1000)
|
|||||||
|
|
||||||
**必选参数 :** `id` : 音乐 id
|
**必选参数 :** `id` : 音乐 id
|
||||||
`level`: 播放音质等级, 分为 `standard` => `标准`,`higher` => `较高`, `exhigh`=>`极高`,
|
`level`: 播放音质等级, 分为 `standard` => `标准`,`higher` => `较高`, `exhigh`=>`极高`,
|
||||||
`lossless`=>`无损`, `hires`=>`Hi-Res`, `jyeffect` => `高清环绕声`, `sky` => `沉浸环绕声`, `dolby` => `杜比全景声`, `jymaster` => `超清母带`
|
`lossless`=>`无损`, `hires`=>`Hi-Res`, `jyeffect` => `高清臻音`, `dolby` => `杜比全景声`, `vivid` => `臻音全景声`, `jymaster` => `超清母带`, `sky` => `沉浸环绕声`
|
||||||
|
|
||||||
**接口地址 :** `/song/download/url/v1`
|
**接口地址 :** `/song/download/url/v1`
|
||||||
|
|
||||||
@@ -4772,6 +4960,23 @@ bitrate = Math.floor(br / 1000)
|
|||||||
|
|
||||||
**接口地址 :** `/listen/data/today/song`
|
**接口地址 :** `/listen/data/today/song`
|
||||||
|
|
||||||
|
### 听歌足迹 - 歌曲播放排行
|
||||||
|
|
||||||
|
说明 : 登录后调用此接口, 获取歌曲播放排行, 返回每首歌的播放次数 (playCount); 默认返回前 20 名 (Top20), 不足则返回全部
|
||||||
|
|
||||||
|
**必选参数 :**
|
||||||
|
|
||||||
|
`type`: 维度类型 周 week 月 month
|
||||||
|
|
||||||
|
**可选参数 :**
|
||||||
|
|
||||||
|
`endTime` : 周: 每周周六 0 点的时间戳 月: 每月最后一天 0 点的时间戳
|
||||||
|
不填就是本周/月的
|
||||||
|
|
||||||
|
**接口地址 :** `/listen/data/song/play/rank`
|
||||||
|
|
||||||
|
**调用例子 :** `/listen/data/song/play/rank?type=month`
|
||||||
|
|
||||||
### 听歌足迹 - 总收听时长
|
### 听歌足迹 - 总收听时长
|
||||||
|
|
||||||
说明 : 登录后调用此接口, 获取总收听时长; 相关接口可能需要 vip 权限
|
说明 : 登录后调用此接口, 获取总收听时长; 相关接口可能需要 vip 权限
|
||||||
@@ -5028,7 +5233,7 @@ let data = encodeURIComponent(
|
|||||||
|
|
||||||
**调用例子:** `/vip/sign`
|
**调用例子:** `/vip/sign`
|
||||||
|
|
||||||
### 黑胶乐签打卡信息
|
### 黑胶乐签未来打卡信息
|
||||||
|
|
||||||
说明: 登录后调用此接口, 获取黑胶乐签打卡信息
|
说明: 登录后调用此接口, 获取黑胶乐签打卡信息
|
||||||
|
|
||||||
@@ -5050,7 +5255,6 @@ let data = encodeURIComponent(
|
|||||||
|
|
||||||
**调用例子:** `/broadcast/sub?id=5&t=1`
|
**调用例子:** `/broadcast/sub?id=5&t=1`
|
||||||
|
|
||||||
|
|
||||||
### 用户的创建歌单列表
|
### 用户的创建歌单列表
|
||||||
|
|
||||||
说明 : 调用此接口, 传入用户id, 获取用户的创建歌单列表
|
说明 : 调用此接口, 传入用户id, 获取用户的创建歌单列表
|
||||||
@@ -5125,7 +5329,6 @@ let data = encodeURIComponent(
|
|||||||
|
|
||||||
**调用例子 :** `/voicelist/my/created`
|
**调用例子 :** `/voicelist/my/created`
|
||||||
|
|
||||||
|
|
||||||
### DIFM电台 - 分类
|
### DIFM电台 - 分类
|
||||||
|
|
||||||
说明: 调用此接口, 获取DIFM电台分类
|
说明: 调用此接口, 获取DIFM电台分类
|
||||||
@@ -5304,7 +5507,7 @@ let data = encodeURIComponent(
|
|||||||
|
|
||||||
**接口地址 :** `/comment/report`
|
**接口地址 :** `/comment/report`
|
||||||
|
|
||||||
**调用例子 :* `/comment/report?id=2058263032&cid=123456789&reason=人身攻击`
|
*_调用例子 :_ `/comment/report?id=2058263032&cid=123456789&reason=人身攻击`
|
||||||
|
|
||||||
### 多级行政区划数据
|
### 多级行政区划数据
|
||||||
|
|
||||||
@@ -5336,6 +5539,431 @@ 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`'
|
||||||
|
|
||||||
|
### 获取广告
|
||||||
|
|
||||||
|
说明 : 调用此接口, 可获取广告
|
||||||
|
|
||||||
|
**接口地址 :** `/ad/get`
|
||||||
|
|
||||||
|
**调用例子 :** `/ad/get`
|
||||||
|
|
||||||
|
### 看广告领取权益(免费听歌时长 / 云贝等)
|
||||||
|
|
||||||
|
说明 : 登录后调用此接口, 领取广告权益。权益类型由广告平台下发的配置决定, 不仅限于 30 分钟免费听歌时长, 还包括"看视频获得最高 2000 云贝"等拉新分段权益(`rightsGainMethod=6`)。除下方常用参数外, 权益类型/时长/扩展权益等其余字段会自动从广告下发配置补齐, 无需手动传入。
|
||||||
|
|
||||||
|
!> 警告: 通过调取接口出现的任何问题由调用者自行承担
|
||||||
|
|
||||||
|
**可选参数 :**
|
||||||
|
|
||||||
|
`reqUid` : 广告请求 ID, 通过 `/ad/get` 获取, 未传时自动获取
|
||||||
|
|
||||||
|
`uid` : 当前登录用户 ID, 不传时服务端从 Cookie 识别
|
||||||
|
|
||||||
|
`rightsGainMethod` : 权益领取方式, `1`: 曝光, `2`: 曝光+点击(默认), `3`: 曝光+下载, `4`: 点击+停留, `5`: 曝光或点击停留, `6`: 拉新曝光/下载分段权益(看视频得云贝)
|
||||||
|
|
||||||
|
`type_ids` : 广告位类型, 默认 `["400002_0"]`
|
||||||
|
|
||||||
|
`creativeType` : 广告创意类型, 激励视频场景为 `36`, 默认 `36`
|
||||||
|
|
||||||
|
**接口地址 :** `/ad/listening/rights/gain`
|
||||||
|
|
||||||
|
**调用例子 :** `/ad/listening/rights/gain`
|
||||||
|
|
||||||
|
### 获取免费听时长状态
|
||||||
|
|
||||||
|
说明 : 登录后调用此接口, 获取免费听剩余时长
|
||||||
|
|
||||||
|
**接口地址 :** `/ad/listening/rights`
|
||||||
|
|
||||||
|
**调用例子 :** `/ad/listening/rights`
|
||||||
|
|
||||||
|
### 云小编 - 获取用户详情
|
||||||
|
|
||||||
|
说明: 登录后调用此接口, 获取云小编用户详情
|
||||||
|
|
||||||
|
**接口地址:** `/rep/ugc/user/get`
|
||||||
|
|
||||||
|
**调用例子:** `/rep/ugc/user/get`
|
||||||
|
|
||||||
|
### 云小编 - 每日签到
|
||||||
|
|
||||||
|
说明: 登录后调用此接口, 进行云小编签到, 领取 5 积分, 签到后 `/rep/ugc/user/get` 返回 `data.signed = 1`
|
||||||
|
|
||||||
|
**接口地址:** `/rep/ugc/user/sign`
|
||||||
|
|
||||||
|
**调用例子:** `/rep/ugc/user/sign`
|
||||||
|
|
||||||
|
### 云小编 - 查询会员任务状态
|
||||||
|
|
||||||
|
说明: 登录后调用此接口, 查询云小编会员任务状态, 当 `data.status = 20` 时, 可调用 `/rep/ugc/user/collect-vip` 领取会员
|
||||||
|
|
||||||
|
**接口地址:** `/rep/ugc/user/vip`
|
||||||
|
|
||||||
|
**调用例子:** `/rep/ugc/user/vip`
|
||||||
|
|
||||||
|
### 云小编 - 活动信息
|
||||||
|
|
||||||
|
说明: 登录后调用此接口, 查询云小编会员活动信息
|
||||||
|
|
||||||
|
**接口地址:** `/rep/ugc/activity/get`
|
||||||
|
|
||||||
|
**调用例子:** `/rep/ugc/activity/get`
|
||||||
|
|
||||||
|
### 云小编 - 获取任务
|
||||||
|
|
||||||
|
> 注意: 调用前请先使用官方客户端完成云小编“情绪标签审核”入站考试
|
||||||
|
|
||||||
|
**可选参数:**
|
||||||
|
|
||||||
|
`type`: 任务类型, 1: 歌曲曲风审核, 2: 歌曲语种审核, 3: 歌曲原唱审核, 4: 情绪标签审核, 默认 `4`
|
||||||
|
|
||||||
|
**接口地址:** `/thinktank/audit/resource/detail`
|
||||||
|
|
||||||
|
**调用例子:** `/thinktank/audit/resource/detail?type=4`
|
||||||
|
|
||||||
|
### 云小编 - 提交任务
|
||||||
|
|
||||||
|
> 注意: 投票结果会在后台审核, 一致 +3 积分, 不一致 -2 积分
|
||||||
|
|
||||||
|
**可选参数:**
|
||||||
|
|
||||||
|
`type`: 任务类型, 1: 歌曲曲风审核, 2: 歌曲语种审核, 3: 歌曲原唱审核, 4: 情绪标签审核, 默认 `4`
|
||||||
|
|
||||||
|
**必选参数:**
|
||||||
|
|
||||||
|
`taskId`: 任务 ID, 调用 `/thinktank/audit/resource/detail` 获取 `data.taskId`
|
||||||
|
|
||||||
|
`judgement`: 审核结果, 1: 同意, 2: 否决, 3: 跳过 (不算次数)
|
||||||
|
|
||||||
|
**接口地址:** `/thinktank/audit/resource/update`
|
||||||
|
|
||||||
|
**调用例子:** `/thinktank/audit/resource/update?type=4&taskId=123456&judgement=1`
|
||||||
|
|
||||||
|
### 云小编 - 领取任务积分
|
||||||
|
|
||||||
|
说明: 完成任务后调用此接口, 领取云小编任务积分
|
||||||
|
|
||||||
|
**可选参数:**
|
||||||
|
|
||||||
|
`activityId`: 活动 ID, 调用 `/rep/ugc/activity/get` 获取, 默认 `5001`
|
||||||
|
|
||||||
|
**接口地址:** `/rep/ugc/activity/collect`
|
||||||
|
|
||||||
|
**调用例子:** `/rep/ugc/activity/collect?activityId=5001`
|
||||||
|
|
||||||
|
### 云小编 - 领取一日会员
|
||||||
|
|
||||||
|
说明: 达成领取条件 (`/rep/ugc/user/vip`) 后调用此接口, 领取一日会员
|
||||||
|
|
||||||
|
**可选参数:**
|
||||||
|
|
||||||
|
`activityId`: 活动 ID, 调用 `/rep/ugc/activity/get` 获取, 默认 `5001`
|
||||||
|
|
||||||
|
**接口地址:** `/rep/ugc/user/collect-vip`
|
||||||
|
|
||||||
|
**调用例子:** `/rep/ugc/user/collect-vip?activityId=5001`
|
||||||
|
|
||||||
|
### 云小编 - 剩余抽奖次数
|
||||||
|
|
||||||
|
说明: 登录后调用此接口, 获取今日云小编抽奖剩余次数
|
||||||
|
|
||||||
|
**可选参数:**
|
||||||
|
|
||||||
|
`activityId`: 活动 ID, 默认 `6501202`
|
||||||
|
|
||||||
|
**接口地址:** `/middle/play/lottery/remain/chance`
|
||||||
|
|
||||||
|
**调用例子:** `/middle/play/lottery/remain/chance?activityId=6501202`
|
||||||
|
|
||||||
|
### 云小编 - 每日抽奖
|
||||||
|
|
||||||
|
说明: 登录后调用此接口, 消耗 200 积分进行抽奖, 每日最多抽 3 次
|
||||||
|
|
||||||
|
> 注意: 抽奖失败也消耗每日次数, 请先调用 `/rep/ugc/user/get` 查询可用积分
|
||||||
|
|
||||||
|
**可选参数:**
|
||||||
|
|
||||||
|
`activityId`: 活动 ID, 默认 `6501202`
|
||||||
|
|
||||||
|
`drawCount`: 未知, 默认 `1`
|
||||||
|
|
||||||
|
`checkToken`: 易盾反作弊 Token, 默认自动获取
|
||||||
|
|
||||||
|
**接口地址:** `/middle/play/do/lottery`
|
||||||
|
|
||||||
|
**调用例子:** `/middle/play/do/lottery?activityId=6501202&drawCount=1`
|
||||||
|
|
||||||
|
### 发送/删除评论
|
||||||
|
|
||||||
|
说明 : 调用此接口,可发送评论或者删除评论
|
||||||
|
|
||||||
|
1. 发送评论
|
||||||
|
|
||||||
|
**必选参数**
|
||||||
|
|
||||||
|
`type`: 数字,资源类型,对应歌曲,mv,专辑,歌单,电台,视频对应以下类型
|
||||||
|
|
||||||
|
```
|
||||||
|
0: 歌曲
|
||||||
|
|
||||||
|
1: mv
|
||||||
|
|
||||||
|
2: 歌单
|
||||||
|
|
||||||
|
3: 专辑
|
||||||
|
|
||||||
|
4: 电台
|
||||||
|
|
||||||
|
5: 视频
|
||||||
|
|
||||||
|
6: 动态
|
||||||
|
```
|
||||||
|
|
||||||
|
`id`: 对应资源 id
|
||||||
|
|
||||||
|
`content`: 要发送的内容
|
||||||
|
|
||||||
|
**调用例子** : `/comment/add?type=1&id=5436712&content=test` (往广岛之恋 mv 发送评论: test)
|
||||||
|
|
||||||
|
2. 回复评论
|
||||||
|
|
||||||
|
**必选参数**
|
||||||
|
|
||||||
|
`type`: 数字,资源类型,对应歌曲,mv,专辑,歌单,电台,视频对应以下类型
|
||||||
|
|
||||||
|
```
|
||||||
|
0: 歌曲
|
||||||
|
|
||||||
|
1: mv
|
||||||
|
|
||||||
|
2: 歌单
|
||||||
|
|
||||||
|
3: 专辑
|
||||||
|
|
||||||
|
4: 电台
|
||||||
|
|
||||||
|
5: 视频
|
||||||
|
|
||||||
|
6: 动态
|
||||||
|
```
|
||||||
|
|
||||||
|
`id`: 对应资源 id
|
||||||
|
|
||||||
|
`cid`: 评论 id
|
||||||
|
|
||||||
|
`content`: 要发送的内容
|
||||||
|
|
||||||
|
**调用例子** : `/comment/add?type=1&id=5436712&cid=1535550516319&content=test` (往广岛之恋 mv 回复test评论: test)
|
||||||
|
|
||||||
|
3. 删除评论
|
||||||
|
|
||||||
|
**必选参数**
|
||||||
|
|
||||||
|
`type`: 数字,资源类型,对应歌曲,mv,专辑,歌单,电台,视频对应以下类型
|
||||||
|
|
||||||
|
```
|
||||||
|
0: 歌曲
|
||||||
|
|
||||||
|
1: mv
|
||||||
|
|
||||||
|
2: 歌单
|
||||||
|
|
||||||
|
3: 专辑
|
||||||
|
|
||||||
|
4: 电台节目
|
||||||
|
|
||||||
|
5: 视频
|
||||||
|
|
||||||
|
6: 动态
|
||||||
|
|
||||||
|
7: 电台
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
`id`: 对应资源 id
|
||||||
|
|
||||||
|
`cid`: 评论 id
|
||||||
|
|
||||||
|
**调用例子** : `/comment?type=1&id=5436712&cid=1535550516319` (在广岛之恋 mv 删除评论)
|
||||||
|
|
||||||
|
### 获取在线设备列表
|
||||||
|
|
||||||
|
说明: 登录后调用此接口, 获取在线设备列表
|
||||||
|
|
||||||
|
**接口地址:** `/device/list`
|
||||||
|
|
||||||
|
**调用例子:** `/device/list`
|
||||||
|
|
||||||
|
### 发送安全验证码
|
||||||
|
|
||||||
|
说明: 登录后调用此接口, 传入手机号, 可发送安全验证码
|
||||||
|
|
||||||
|
**必选参数 :** `phone`: 手机号
|
||||||
|
|
||||||
|
**接口地址 :** `/captcha/safe/sent`
|
||||||
|
|
||||||
|
**调用例子 :** `/captcha/safe/sent?phone=13XXXXXXXXX`
|
||||||
|
|
||||||
|
### 强制下线设备
|
||||||
|
|
||||||
|
说明: 登录后调用此接口, 传入设备 id, 可强制下线设备的登录会话
|
||||||
|
|
||||||
|
**必选参数 :** `key`: 设备的 `deviceKey`, 可通过 `/device/list` 获取
|
||||||
|
|
||||||
|
`captcha`: 安全验证码, 可通过 `/captcha/safe/sent` 获取
|
||||||
|
|
||||||
|
**接口地址 :** `/device/kickoff`
|
||||||
|
|
||||||
|
**调用例子 :** `/device/kickoff?key=00ALDFGEXXXXXXXXXXXXXXXXX&captcha=1234`
|
||||||
|
|
||||||
|
### 上报设备中心设备名称
|
||||||
|
|
||||||
|
说明: 登录后调用此接口, 传入 `deviceName`, 可修改当前设备在网易云登录设备管理列表里显示的名称
|
||||||
|
|
||||||
|
**必选参数 :** `deviceName`: 期望展示的设备名称
|
||||||
|
|
||||||
|
`name`: `deviceName` 的兼容别名, 二选一即可
|
||||||
|
|
||||||
|
**接口地址 :** `/deviceinfo/center/upload`
|
||||||
|
|
||||||
|
**调用例子 :** `/deviceinfo/center/upload?deviceName=XXXXX`
|
||||||
|
|
||||||
|
### 插播相似歌曲
|
||||||
|
|
||||||
|
说明: 调用此接口, 传入歌曲 id, 可获取插播相似歌曲
|
||||||
|
|
||||||
|
**必选参数 :** `id`: 歌曲 id
|
||||||
|
|
||||||
|
**接口地址 :** `/song/simi/get`
|
||||||
|
|
||||||
|
**调用例子 :** `/song/simi/get?id=39227633`
|
||||||
|
|
||||||
|
### 获取用户乐迷团列表
|
||||||
|
|
||||||
|
说明 : 登录后调用此接口 , 可获取当前登录用户已加入的全部歌手乐迷团列表
|
||||||
|
|
||||||
|
**接口地址 :** `/fans/group/user/groups`
|
||||||
|
|
||||||
|
**调用例子 :** `/fans/group/user/groups`
|
||||||
|
|
||||||
|
### 获取乐迷团详情
|
||||||
|
|
||||||
|
说明 : 调用此接口 , 可获取乐迷团详情
|
||||||
|
|
||||||
|
**必选参数 :** `groupId` : 乐迷团 ID
|
||||||
|
|
||||||
|
**可选参数 :** `scene` : 场景标识
|
||||||
|
|
||||||
|
**接口地址 :** `/fans/group/detail`
|
||||||
|
|
||||||
|
**调用例子 :** `/fans/group/detail?groupId=1872529203038486609`
|
||||||
|
|
||||||
|
### 获取用户在乐迷团的详情
|
||||||
|
|
||||||
|
说明 : 登录后调用此接口 , 可获取当前用户在指定乐迷团的详细成员信息及铭牌等级
|
||||||
|
|
||||||
|
**必选参数 :** `groupId` : 乐迷团 ID
|
||||||
|
|
||||||
|
**接口地址 :** `/fans/group/user/group/detail`
|
||||||
|
|
||||||
|
**调用例子 :** `/fans/group/user/group/detail?groupId=1755777360858267711`
|
||||||
|
|
||||||
|
### 获取乐迷团推荐笔记
|
||||||
|
|
||||||
|
说明 : 调用此接口 , 可获取指定乐迷团的推荐笔记
|
||||||
|
|
||||||
|
**必选参数 :** `fansGroupId` : 乐迷团 ID
|
||||||
|
|
||||||
|
**可选参数 :** `cursor` : 游标 , 默认为 0
|
||||||
|
|
||||||
|
`size` : 返回数量 , 默认为 10
|
||||||
|
|
||||||
|
**接口地址 :** `/fans/group/feed/recommend`
|
||||||
|
|
||||||
|
**调用例子 :** `/fans/group/feed/recommend?fansGroupId=1872529203038486609&size=10&cursor=0`
|
||||||
|
|
||||||
|
### 每日指定风格歌曲推荐
|
||||||
|
|
||||||
|
说明 : 调用此接口 , 可获取每日指定风格歌曲推荐
|
||||||
|
|
||||||
|
**接口地址 :** `/recommend/category/songs`
|
||||||
|
|
||||||
|
**调用例子 :** `/recommend/category/songs`
|
||||||
|
|
||||||
|
### 获取每日指定风格歌曲推荐的风格列表
|
||||||
|
|
||||||
|
说明 : 调用此接口 , 可获取每日指定风格歌曲推荐的风格列表
|
||||||
|
|
||||||
|
**接口地址 :** `/recommend/category/configs`
|
||||||
|
|
||||||
|
**调用例子 :** `/recommend/category/configs`
|
||||||
|
|
||||||
|
### 设置每日指定风格歌曲推荐的风格
|
||||||
|
|
||||||
|
说明 : 调用此接口 , 可设置每日指定风格歌曲推荐的风格
|
||||||
|
|
||||||
|
**必选参数 :**
|
||||||
|
|
||||||
|
`tags`: 风格标签, 可通过 `/recommend/category/configs` 获取
|
||||||
|
|
||||||
|
`category`: 风格标签的父ID, 可通过 `/recommend/category/configs` 获取
|
||||||
|
|
||||||
|
!> 只能设置当前CategoryId下的tags, 不能设置其他CategoryId下的tags, 否则会报错
|
||||||
|
|
||||||
|
**接口地址 :** `/recommend/category/config`
|
||||||
|
|
||||||
|
**调用例子 :** `/recommend/category/config?tags=10004,10015&categoryId=1000`
|
||||||
|
|
||||||
## 离线访问此文档
|
## 离线访问此文档
|
||||||
|
|
||||||
此文档同时也是 Progressive Web Apps(PWA), 加入了 serviceWorker, 可离线访问
|
此文档同时也是 Progressive Web Apps(PWA), 加入了 serviceWorker, 可离线访问
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user