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 | ||
|
|
a9c33de182 | ||
|
|
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 }}
|
||||||
@@ -260,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
|
||||||
|
|||||||
+2
-1
@@ -9,4 +9,5 @@ bin
|
|||||||
anonymous_token
|
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` 白名单;改依赖安装行为时先看它。
|
||||||
+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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -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 生态
|
||||||
|
|
||||||
| 语言 | 作者 | 地址 | 类型 |
|
| 语言 | 作者 | 地址 | 类型 |
|
||||||
|
|||||||
@@ -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
+353
@@ -37,6 +37,41 @@ export const enum SubAction {
|
|||||||
unsub = 0,
|
unsub = 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function ad_get(
|
||||||
|
params: { type_ids?: string } & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function ad_listening_rights(
|
||||||
|
params: RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function ad_listening_rights_gain(
|
||||||
|
params: {
|
||||||
|
reqUid?: string
|
||||||
|
uid?: string | number
|
||||||
|
exposureTime?: string | number
|
||||||
|
clickTime?: string | number
|
||||||
|
extraRightsType?: string | number
|
||||||
|
playContinuously?: boolean | string
|
||||||
|
source?: string | number
|
||||||
|
creativeType?: string | number
|
||||||
|
rightsGainMethod?: string | number
|
||||||
|
extraRightsGainMethod?: string | number
|
||||||
|
extraRightsGainDuration?: string | number
|
||||||
|
nextRightsGainDuration?: string | number
|
||||||
|
rightsGainType?: string | number
|
||||||
|
rightsGainDuration?: string | number
|
||||||
|
gainMethodStep?: string | number
|
||||||
|
generalRightsInfo?: string
|
||||||
|
rightsExtJson?: string
|
||||||
|
appInfo?: string
|
||||||
|
contextInfo?: string
|
||||||
|
installed?: string | number
|
||||||
|
sniffTime?: string | number
|
||||||
|
type_ids?: string
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
export function activate_init_profile(
|
export function activate_init_profile(
|
||||||
params: { nickname: string } & RequestBaseConfig,
|
params: { nickname: string } & RequestBaseConfig,
|
||||||
): Promise<Response>
|
): Promise<Response>
|
||||||
@@ -119,6 +154,14 @@ export function album_sublist(
|
|||||||
params: MultiPageConfig & RequestBaseConfig,
|
params: MultiPageConfig & RequestBaseConfig,
|
||||||
): Promise<Response>
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function api(
|
||||||
|
params: {
|
||||||
|
uri: string
|
||||||
|
data?: string | Record<string, unknown>
|
||||||
|
crypto?: string
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
export function artist_album(
|
export function artist_album(
|
||||||
params: { id: string | number } & MultiPageConfig & RequestBaseConfig,
|
params: { id: string | number } & MultiPageConfig & RequestBaseConfig,
|
||||||
): Promise<Response>
|
): Promise<Response>
|
||||||
@@ -271,10 +314,21 @@ export function batch(
|
|||||||
params: { [index: string]: unknown } & RequestBaseConfig,
|
params: { [index: string]: unknown } & RequestBaseConfig,
|
||||||
): Promise<Response>
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function captcha_safe_sent(
|
||||||
|
params: { ctcode?: number | string } & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
export function captcha_sent(
|
export function captcha_sent(
|
||||||
params: { phone: string; ctcode?: number | string } & RequestBaseConfig,
|
params: { phone: string; ctcode?: number | string } & RequestBaseConfig,
|
||||||
): Promise<Response>
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function captcha_sent_v1(
|
||||||
|
params: {
|
||||||
|
phone: number | string
|
||||||
|
ctcode?: number | string
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
export function captcha_verify(
|
export function captcha_verify(
|
||||||
params: {
|
params: {
|
||||||
ctcode?: number | string
|
ctcode?: number | string
|
||||||
@@ -387,6 +441,14 @@ export function comment(
|
|||||||
} & RequestBaseConfig,
|
} & RequestBaseConfig,
|
||||||
): Promise<Response>
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function comment_add(
|
||||||
|
params: {
|
||||||
|
id: string | number
|
||||||
|
type: CommentType
|
||||||
|
content: string
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
export function comment_album(
|
export function comment_album(
|
||||||
params: {
|
params: {
|
||||||
id: string | number
|
id: string | number
|
||||||
@@ -395,6 +457,14 @@ export function comment_album(
|
|||||||
RequestBaseConfig,
|
RequestBaseConfig,
|
||||||
): Promise<Response>
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function comment_delete(
|
||||||
|
params: {
|
||||||
|
id: string | number
|
||||||
|
type: CommentType
|
||||||
|
cid: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
export function comment_dj(
|
export function comment_dj(
|
||||||
params: {
|
params: {
|
||||||
id: string | number
|
id: string | number
|
||||||
@@ -469,6 +539,15 @@ export function comment_playlist(
|
|||||||
RequestBaseConfig,
|
RequestBaseConfig,
|
||||||
): Promise<Response>
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function comment_reply(
|
||||||
|
params: {
|
||||||
|
id: string | number
|
||||||
|
type: CommentType
|
||||||
|
cid: string | number
|
||||||
|
content: string
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
export function comment_video(
|
export function comment_video(
|
||||||
params: {
|
params: {
|
||||||
id: string | number
|
id: string | number
|
||||||
@@ -490,6 +569,25 @@ export function daily_signin(
|
|||||||
params: { type?: DailySigninType } & RequestBaseConfig,
|
params: { type?: DailySigninType } & RequestBaseConfig,
|
||||||
): Promise<Response>
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function decrypt(
|
||||||
|
params: {
|
||||||
|
crypto?: string
|
||||||
|
data?: string
|
||||||
|
hexString?: string
|
||||||
|
isReq?: boolean | string
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function device_kickoff(
|
||||||
|
params: { deviceKey: string | number; captcha?: string } & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function device_list(params: RequestBaseConfig): Promise<Response>
|
||||||
|
|
||||||
|
export function deviceinfo_center_upload(
|
||||||
|
params: { deviceName: string; name?: string } & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
export function digitalAlbum_ordering(
|
export function digitalAlbum_ordering(
|
||||||
params: {
|
params: {
|
||||||
payment: string
|
payment: string
|
||||||
@@ -618,6 +716,10 @@ export function dj_toplist_popular(
|
|||||||
params: { limit?: string | number } & RequestBaseConfig,
|
params: { limit?: string | number } & RequestBaseConfig,
|
||||||
): Promise<Response>
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function eapi_decrypt(
|
||||||
|
params: { hexString: string; isReq?: boolean | string } & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
export function event(
|
export function event(
|
||||||
params: { pagesize?: number; lasttime?: number } & RequestBaseConfig,
|
params: { pagesize?: number; lasttime?: number } & RequestBaseConfig,
|
||||||
): Promise<Response>
|
): Promise<Response>
|
||||||
@@ -626,6 +728,20 @@ export function event_del(
|
|||||||
params: { evId: string | number } & RequestBaseConfig,
|
params: { evId: string | number } & RequestBaseConfig,
|
||||||
): Promise<Response>
|
): Promise<Response>
|
||||||
|
|
||||||
|
export const enum EventPrivacy {
|
||||||
|
everyone = 0,
|
||||||
|
following = 1,
|
||||||
|
onlyMe = 2,
|
||||||
|
mutualFollowing = 6,
|
||||||
|
}
|
||||||
|
|
||||||
|
export function event_privacy(
|
||||||
|
params: {
|
||||||
|
evId: string | number
|
||||||
|
privacy: EventPrivacy
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
export function event_forward(
|
export function event_forward(
|
||||||
params: {
|
params: {
|
||||||
forwords: string
|
forwords: string
|
||||||
@@ -665,6 +781,8 @@ export function hot_topic(
|
|||||||
params: MultiPageConfig & RequestBaseConfig,
|
params: MultiPageConfig & RequestBaseConfig,
|
||||||
): Promise<Response>
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function inner_version(params: RequestBaseConfig): Promise<Response>
|
||||||
|
|
||||||
export function like(
|
export function like(
|
||||||
params: {
|
params: {
|
||||||
like?: 'true' | 'false' | boolean
|
like?: 'true' | 'false' | boolean
|
||||||
@@ -678,6 +796,10 @@ export function likelist(
|
|||||||
params: { uid: string | number } & RequestBaseConfig,
|
params: { uid: string | number } & RequestBaseConfig,
|
||||||
): Promise<Response>
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function listentogether_status(
|
||||||
|
params: RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
export function login(
|
export function login(
|
||||||
params: { email: string; password: string } & RequestBaseConfig,
|
params: { email: string; password: string } & RequestBaseConfig,
|
||||||
): Promise<Response>
|
): Promise<Response>
|
||||||
@@ -724,6 +846,17 @@ export function lyric_new(
|
|||||||
params: { id: string | number } & RequestBaseConfig,
|
params: { id: string | number } & RequestBaseConfig,
|
||||||
): Promise<Response>
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function middle_play_do_lottery(
|
||||||
|
params: {
|
||||||
|
activityId?: string | number
|
||||||
|
drawCount?: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function middle_play_lottery_remain_chance(
|
||||||
|
params: { activityId?: string | number } & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
export function msg_comments(
|
export function msg_comments(
|
||||||
params: {
|
params: {
|
||||||
uid: string | number
|
uid: string | number
|
||||||
@@ -946,6 +1079,22 @@ export function register_cellphone(
|
|||||||
} & RequestBaseConfig,
|
} & RequestBaseConfig,
|
||||||
): Promise<Response>
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function register_checktoken_v2(
|
||||||
|
params: { refresh?: boolean | string } & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function register_checktoken_v3(
|
||||||
|
params: { refresh?: boolean | string } & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function register_neapikey(
|
||||||
|
params: { version?: string | number; behavior?: string } & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function register_xeapikey(
|
||||||
|
params: { deviceId?: string; currentKeyVersion?: string } & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
export function related_allvideo(
|
export function related_allvideo(
|
||||||
params: { id: string | number } & RequestBaseConfig,
|
params: { id: string | number } & RequestBaseConfig,
|
||||||
): Promise<Response>
|
): Promise<Response>
|
||||||
@@ -954,6 +1103,67 @@ export function related_playlist(
|
|||||||
params: { id: string | number } & RequestBaseConfig,
|
params: { id: string | number } & RequestBaseConfig,
|
||||||
): Promise<Response>
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function relay_play_state_submit(
|
||||||
|
params: {
|
||||||
|
id: string | number
|
||||||
|
sessionId?: string
|
||||||
|
progress?: string | number
|
||||||
|
playMode?: string
|
||||||
|
type?: string
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function rep_ugc_activity_collect(
|
||||||
|
params: { activityId?: string | number } & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function rep_ugc_activity_get(
|
||||||
|
params: RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function rep_ugc_exam_info_get(
|
||||||
|
params: { examType: string | number } & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function rep_ugc_exam_question_single_get(
|
||||||
|
params: {
|
||||||
|
examType: string | number
|
||||||
|
taskId: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function rep_ugc_exam_result_get(
|
||||||
|
params: {
|
||||||
|
examType: string | number
|
||||||
|
taskId: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function rep_ugc_exam_start(
|
||||||
|
params: { examType: string | number } & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function rep_ugc_exam_submit(
|
||||||
|
params: {
|
||||||
|
examType: string | number
|
||||||
|
taskId: string | number
|
||||||
|
questionId: string | number
|
||||||
|
answer: string
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
declare const rep_ugc_user_collect_vip: (
|
||||||
|
params: { activityId?: string | number } & RequestBaseConfig,
|
||||||
|
) => Promise<Response>
|
||||||
|
|
||||||
|
export { rep_ugc_user_collect_vip as 'rep_ugc_user_collect-vip' }
|
||||||
|
|
||||||
|
export function rep_ugc_user_get(params: RequestBaseConfig): Promise<Response>
|
||||||
|
|
||||||
|
export function rep_ugc_user_sign(params: RequestBaseConfig): Promise<Response>
|
||||||
|
|
||||||
|
export function rep_ugc_user_vip(params: RequestBaseConfig): Promise<Response>
|
||||||
|
|
||||||
export const enum ResourceType {
|
export const enum ResourceType {
|
||||||
mv = 1,
|
mv = 1,
|
||||||
dj = 4,
|
dj = 4,
|
||||||
@@ -979,6 +1189,22 @@ export function scrobble(
|
|||||||
} & RequestBaseConfig,
|
} & RequestBaseConfig,
|
||||||
): Promise<Response>
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function scrobble_v1(
|
||||||
|
params: {
|
||||||
|
id: string | number
|
||||||
|
time: string | number
|
||||||
|
total?: string | number
|
||||||
|
sourceid?: string | number
|
||||||
|
sourceId?: string | number
|
||||||
|
source?: string
|
||||||
|
name?: string
|
||||||
|
artist?: string
|
||||||
|
bitrate?: string | number
|
||||||
|
level?: string
|
||||||
|
vip?: boolean | string
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
export function search(
|
export function search(
|
||||||
params: {
|
params: {
|
||||||
keywords: string
|
keywords: string
|
||||||
@@ -1160,6 +1386,7 @@ export const enum SoundQualityType {
|
|||||||
jyeffect = 'jyeffect',
|
jyeffect = 'jyeffect',
|
||||||
jymaster = 'jymaster',
|
jymaster = 'jymaster',
|
||||||
sky = 'sky',
|
sky = 'sky',
|
||||||
|
vivid = 'vivid',
|
||||||
}
|
}
|
||||||
|
|
||||||
export function song_url_v1(
|
export function song_url_v1(
|
||||||
@@ -1267,6 +1494,25 @@ export function user_event(
|
|||||||
} & RequestBaseConfig,
|
} & RequestBaseConfig,
|
||||||
): Promise<Response>
|
): Promise<Response>
|
||||||
|
|
||||||
|
export interface UserEventAllResponse {
|
||||||
|
code: number
|
||||||
|
events: Record<string, unknown>[]
|
||||||
|
/** Upstream account statistic; it can include events no longer returned. */
|
||||||
|
size: number | null
|
||||||
|
/** Number of unique events in `events`. */
|
||||||
|
retrievedCount: number
|
||||||
|
/** Positive difference between `size` and `retrievedCount`. */
|
||||||
|
unavailableCount: number | null
|
||||||
|
sizeMismatch: boolean | null
|
||||||
|
pageCount: number
|
||||||
|
more: false
|
||||||
|
lasttime: string | number | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function user_event_all(
|
||||||
|
params: RequestBaseConfig,
|
||||||
|
): Promise<Response<UserEventAllResponse>>
|
||||||
|
|
||||||
export function user_followeds(
|
export function user_followeds(
|
||||||
params: {
|
params: {
|
||||||
uid: string | number
|
uid: string | number
|
||||||
@@ -1433,6 +1679,23 @@ export function yunbei_task_finish(
|
|||||||
} & RequestBaseConfig,
|
} & RequestBaseConfig,
|
||||||
): Promise<Response>
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function yunbei_ad_task_list(
|
||||||
|
params: RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function yunbei_ad_task_recommend_song(
|
||||||
|
params: {
|
||||||
|
offset?: number | string
|
||||||
|
limit?: number | string
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function yunbei_ad_task_finish(
|
||||||
|
params: {
|
||||||
|
yunbeiAmount?: number | string
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
export function msg_recentcontact(params: RequestBaseConfig): Promise<Response>
|
export function msg_recentcontact(params: RequestBaseConfig): Promise<Response>
|
||||||
|
|
||||||
export function hug_comment(
|
export function hug_comment(
|
||||||
@@ -1474,6 +1737,20 @@ export function artist_new_song(
|
|||||||
} & RequestBaseConfig,
|
} & RequestBaseConfig,
|
||||||
): Promise<Response>
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function artist_new_song_playall(
|
||||||
|
params: RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function artist_new_song_mv_list_v2(
|
||||||
|
params: {
|
||||||
|
startTimestamp?: number | string
|
||||||
|
before?: number | string
|
||||||
|
sourceType?: number | string
|
||||||
|
limit?: number | string
|
||||||
|
firstRequest?: boolean | string
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
export function artist_detail(
|
export function artist_detail(
|
||||||
params: {
|
params: {
|
||||||
id: number | string
|
id: number | string
|
||||||
@@ -1594,6 +1871,10 @@ export function vip_growthpoint_get(
|
|||||||
} & RequestBaseConfig,
|
} & RequestBaseConfig,
|
||||||
): Promise<Response>
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function vip_growthpoint_getall(
|
||||||
|
params: RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
export function artist_fans(
|
export function artist_fans(
|
||||||
params: { id: number | string } & MultiPageConfig & RequestBaseConfig,
|
params: { id: number | string } & MultiPageConfig & RequestBaseConfig,
|
||||||
): Promise<Response>
|
): Promise<Response>
|
||||||
@@ -2049,6 +2330,13 @@ export function listen_data_report(
|
|||||||
} & RequestBaseConfig,
|
} & RequestBaseConfig,
|
||||||
): Promise<Response>
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function listen_data_song_play_rank(
|
||||||
|
params: {
|
||||||
|
type?: string
|
||||||
|
endTime?: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
export function listen_data_today_song(
|
export function listen_data_today_song(
|
||||||
params: RequestBaseConfig,
|
params: RequestBaseConfig,
|
||||||
): Promise<Response>
|
): Promise<Response>
|
||||||
@@ -2267,6 +2555,10 @@ export function song_chorus(
|
|||||||
} & RequestBaseConfig,
|
} & RequestBaseConfig,
|
||||||
): Promise<Response>
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function song_cloud_download(
|
||||||
|
params: { id: string | number } & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
export function song_creators(
|
export function song_creators(
|
||||||
params: {
|
params: {
|
||||||
id: string | number
|
id: string | number
|
||||||
@@ -2378,6 +2670,18 @@ export function threshold_detail_get(
|
|||||||
params: RequestBaseConfig,
|
params: RequestBaseConfig,
|
||||||
): Promise<Response>
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function thinktank_audit_resource_detail(
|
||||||
|
params: { type?: string | number } & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function thinktank_audit_resource_update(
|
||||||
|
params: {
|
||||||
|
type?: string | number
|
||||||
|
taskId: string | number
|
||||||
|
judgement: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
export function toplist_detail_v2(params: RequestBaseConfig): Promise<Response>
|
export function toplist_detail_v2(params: RequestBaseConfig): Promise<Response>
|
||||||
|
|
||||||
export function ugc_album_get(
|
export function ugc_album_get(
|
||||||
@@ -2504,6 +2808,14 @@ export function verify_qrcodestatus(
|
|||||||
|
|
||||||
export function vip_sign(params: RequestBaseConfig): Promise<Response>
|
export function vip_sign(params: RequestBaseConfig): Promise<Response>
|
||||||
|
|
||||||
|
export function vip_sign_detail(
|
||||||
|
params: { timestamp: string | number } & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function vip_sign_history(
|
||||||
|
params: { type?: string | number } & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
export function vip_sign_info(params: RequestBaseConfig): Promise<Response>
|
export function vip_sign_info(params: RequestBaseConfig): Promise<Response>
|
||||||
|
|
||||||
export function vip_tasks_v1(
|
export function vip_tasks_v1(
|
||||||
@@ -2524,6 +2836,22 @@ export function voice_upload(
|
|||||||
name: string
|
name: string
|
||||||
data: string | Buffer
|
data: string | Buffer
|
||||||
}
|
}
|
||||||
|
imgFile?: {
|
||||||
|
name: string
|
||||||
|
data: string | Buffer
|
||||||
|
}
|
||||||
|
voiceListId: string | number
|
||||||
|
coverImgId?: string | number
|
||||||
|
categoryId: string | number
|
||||||
|
secondCategoryId: string | number
|
||||||
|
description: string
|
||||||
|
songName?: string
|
||||||
|
privacy?: string | number
|
||||||
|
publishTime?: string | number
|
||||||
|
autoPublish?: string | number
|
||||||
|
autoPublishText?: string
|
||||||
|
orderNo?: string | number
|
||||||
|
composedSongs?: string
|
||||||
} & RequestBaseConfig,
|
} & RequestBaseConfig,
|
||||||
): Promise<Response>
|
): Promise<Response>
|
||||||
|
|
||||||
@@ -2562,3 +2890,28 @@ export function voicelist_trans(
|
|||||||
} & MultiPageConfig &
|
} & MultiPageConfig &
|
||||||
RequestBaseConfig,
|
RequestBaseConfig,
|
||||||
): Promise<Response>
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function fans_group_detail(
|
||||||
|
params: {
|
||||||
|
groupId: string
|
||||||
|
scene?: string
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function fans_group_feed_recommend(
|
||||||
|
params: {
|
||||||
|
fansGroupId: string
|
||||||
|
cursor?: string
|
||||||
|
size?: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function fans_group_user_groups(
|
||||||
|
params?: RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function fans_group_user_group_detail(
|
||||||
|
params: {
|
||||||
|
groupId: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,7 +17,7 @@ module.exports = async (query, request) => {
|
|||||||
const currentKeyVersion = query.currentKeyVersion || ''
|
const currentKeyVersion = query.currentKeyVersion || ''
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
appVersion: '9.1.65',
|
appVersion: '9.5.61',
|
||||||
currentKeyVersion,
|
currentKeyVersion,
|
||||||
deviceId,
|
deviceId,
|
||||||
nonce,
|
nonce,
|
||||||
@@ -32,10 +32,10 @@ module.exports = async (query, request) => {
|
|||||||
|
|
||||||
const res = await axios({
|
const res = await axios({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: APP_CONF.apiDomain + '/api/gorilla/anti/crawler/security/key/get',
|
url: APP_CONF.apiDomain + '/api/bsr/sk/get',
|
||||||
headers: {
|
headers: {
|
||||||
'User-Agent':
|
'User-Agent':
|
||||||
'NeteaseMusic/9.1.65.240927161425(9001065);Dalvik/2.1.0 (Linux; U; Android 14; 23013RK75C Build/UKQ1.230804.001)',
|
'NeteaseMusic/9.5.61.260802021928(9005061);Dalvik/2.1.0 (Linux; U; Android 12; HBN-AL00 Build/cd737a2.0)',
|
||||||
Cookie: deviceId ? `deviceId=${encodeURIComponent(deviceId)}` : '',
|
Cookie: deviceId ? `deviceId=${encodeURIComponent(deviceId)}` : '',
|
||||||
},
|
},
|
||||||
data: new URLSearchParams(data).toString(),
|
data: new URLSearchParams(data).toString(),
|
||||||
|
|||||||
@@ -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'),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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'),
|
||||||
|
)
|
||||||
|
}
|
||||||
+34
-5
@@ -1,11 +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(
|
const results = {}
|
||||||
`/api/vip-center-bff/task/sign`,
|
|
||||||
data,
|
const taskSign = await request(
|
||||||
|
'/api/vip-center-bff/task/sign',
|
||||||
|
{},
|
||||||
createOption(query, 'weapi'),
|
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,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
// 黑胶乐签打卡历史
|
// 黑胶乐签打卡历史 / 状态查询
|
||||||
|
// 支持传入 type=0(用户信息栏)或 type=1(黑胶乐签)
|
||||||
|
|
||||||
const createOption = require('../util/option.js')
|
const createOption = require('../util/option.js')
|
||||||
module.exports = (query, request) => {
|
module.exports = (query, request) => {
|
||||||
const data = {
|
const data = {
|
||||||
type: '0',
|
type: query.type || '0',
|
||||||
}
|
}
|
||||||
return request(
|
return request(
|
||||||
`/api/vipnewcenter/app/minidesk/music/sign/pc`,
|
`/api/vipnewcenter/app/minidesk/music/sign/pc`,
|
||||||
data,
|
data,
|
||||||
createOption(query, 'xeapi'),
|
createOption(query, 'eapi'),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+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()
|
||||||
+23
-19
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@neteasecloudmusicapienhanced/api",
|
"name": "@neteasecloudmusicapienhanced/api",
|
||||||
"version": "4.35.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": {
|
||||||
@@ -73,44 +75,46 @@
|
|||||||
"data"
|
"data"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@neteasecloudmusicapienhanced/unblockmusic-utils": "^0.3.2",
|
"@neteasecloudmusicapienhanced/unblockmusic-utils": "^0.4.5",
|
||||||
"axios": "^1.17.0",
|
"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.1",
|
"@typescript-eslint/eslint-plugin": "^8.71.0",
|
||||||
"@typescript-eslint/parser": "^8.60.1",
|
"@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": {
|
"publishConfig": {
|
||||||
|
|||||||
Generated
+709
-362
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'));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -11,4 +11,4 @@
|
|||||||
[前往本家](https://github.com/binaryify/NeteaseCloudMusicApi)
|
[前往本家](https://github.com/binaryify/NeteaseCloudMusicApi)
|
||||||
[快速开始](#neteasecloudmusicapienhanced)
|
[快速开始](#neteasecloudmusicapienhanced)
|
||||||
|
|
||||||

|

|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 14 KiB |
+611
-47
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,496 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<title>免费听 — 网易云音乐 API Enhanced</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--fg: #333;
|
||||||
|
--muted: #666;
|
||||||
|
--border: #ddd;
|
||||||
|
--bg: #f5f5f5;
|
||||||
|
--panel: #ffffff;
|
||||||
|
--accent: #333;
|
||||||
|
}
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
html, body { height: 100%; }
|
||||||
|
body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; color: var(--fg); background: var(--bg); line-height: 1.6; }
|
||||||
|
.container { max-width: 800px; margin: 40px auto; padding: 0 20px; }
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.container { margin: 20px auto; padding: 0 16px; }
|
||||||
|
header.site-header h1 { font-size: 22px; }
|
||||||
|
.block { padding: 16px; }
|
||||||
|
}
|
||||||
|
header.site-header { margin-bottom: 24px; }
|
||||||
|
header.site-header h1 { font-size: 24px; font-weight: 600; margin: 0; }
|
||||||
|
.sub { margin-top: 4px; color: var(--muted); font-size: 14px; }
|
||||||
|
.block { background: var(--panel); border: 1px solid var(--border); border-radius: 12px; padding: 20px; margin-bottom: 16px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); }
|
||||||
|
.block h3 { margin: 0 0 4px; font-size: 15px; font-weight: 600; }
|
||||||
|
.block .desc { margin: 0 0 12px; color: var(--muted); font-size: 13px; }
|
||||||
|
pre { margin: 0; background: #f9f9f9; border: 1px solid var(--border); border-radius: 6px; padding: 12px; overflow-x: auto; white-space: pre-wrap; word-break: break-all; font-family: 'Courier New', monospace; font-size: 13px; max-height: 200px; overflow: auto; }
|
||||||
|
pre.data { max-height: 360px; }
|
||||||
|
.row { display: flex; gap: 8px; margin-top: 12px; flex-wrap: wrap; }
|
||||||
|
button { padding: 8px 18px; border-radius: 8px; border: 1px solid var(--border); font-size: 14px; font-weight: 500; cursor: pointer; background: var(--panel); color: var(--fg); transition: all .15s; }
|
||||||
|
button:hover { background: #eee; }
|
||||||
|
button.primary { background: var(--fg); color: var(--panel); border-color: var(--fg); }
|
||||||
|
button.primary:hover { opacity: .85; }
|
||||||
|
button:disabled { opacity: .4; cursor: not-allowed; }
|
||||||
|
button.danger { background: #a11; color: #fff; border-color: #a11; }
|
||||||
|
button.danger:hover { opacity: .85; }
|
||||||
|
.tag { display: inline-block; padding: 2px 10px; border-radius: 20px; font-size: 12px; font-weight: 500; border: 1px solid var(--border); }
|
||||||
|
.tag.ok { background: #e6f7e6; color: #1a7a1a; border-color: #b7e6b7; }
|
||||||
|
.tag.no { background: #ffe6e6; color: #a11; border-color: #f5c6c6; }
|
||||||
|
.tag.busy { background: #fff3cd; color: #856404; border-color: #ffeeba; }
|
||||||
|
.flex { display: flex; align-items: center; gap: 10px; }
|
||||||
|
.flex.sb { justify-content: space-between; }
|
||||||
|
.step { display: flex; gap: 12px; align-items: flex-start; }
|
||||||
|
.step .num { width: 26px; height: 26px; border-radius: 50%; background: var(--fg); color: var(--panel); display: flex; align-items: center; justify-content: center; font-size: 13px; font-weight: 600; flex-shrink: 0; margin-top: 1px; }
|
||||||
|
.step .num.done { background: #1a7a1a; }
|
||||||
|
.step .body { flex: 1; min-width: 0; }
|
||||||
|
input[type=text] { width: 100%; padding: 8px 12px; border-radius: 6px; border: 1px solid var(--border); font-size: 14px; font-family: inherit; margin-top: 6px; }
|
||||||
|
input[type=text]:focus { outline: 2px solid var(--fg); outline-offset: -1px; border-color: transparent; }
|
||||||
|
footer.site-footer { margin-top: 32px; padding-top: 12px; border-top: 1px solid var(--border); color: var(--muted); text-align: center; font-size: 13px; }
|
||||||
|
footer.site-footer a { color: var(--fg); text-decoration: none; }
|
||||||
|
footer.site-footer a:hover { border-bottom: 1px dotted var(--fg); }
|
||||||
|
.info-grid { display: grid; grid-template-columns: 80px 1fr; gap: 4px 12px; font-size: 13px; margin-top: 8px; }
|
||||||
|
.info-grid .lbl { color: var(--muted); }
|
||||||
|
|
||||||
|
/* ---- 剩余时长计时器 ---- */
|
||||||
|
.timer-wrap { text-align: center; padding: 12px 0 4px; }
|
||||||
|
.timer-row { display: flex; align-items: baseline; justify-content: center; gap: 2px; font-variant-numeric: tabular-nums; }
|
||||||
|
.timer-num { font-size: 36px; font-weight: 700; letter-spacing: 1px; line-height: 1.2; font-family: 'SF Mono', 'Courier New', monospace; min-width: 2ch; text-align: center; }
|
||||||
|
.timer-label { font-size: 13px; color: var(--muted); margin-right: 12px; }
|
||||||
|
.timer-label:last-child { margin-right: 0; }
|
||||||
|
.timer-sep { font-size: 28px; font-weight: 300; color: #bbb; margin: 0 2px; }
|
||||||
|
.timer-unit { display: flex; flex-direction: column; align-items: center; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main class="container">
|
||||||
|
<header class="site-header">
|
||||||
|
<h1>🎧 免费听 · 看广告获取时长</h1>
|
||||||
|
<p class="sub">模拟 App 内看激励视频广告 → 领取免费听权益的完整流程</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- 步骤 1: 注册 Token -->
|
||||||
|
<div class="block">
|
||||||
|
<div class="step">
|
||||||
|
<div class="num" id="s1num">1</div>
|
||||||
|
<div class="body">
|
||||||
|
<div class="flex sb">
|
||||||
|
<h3>注册反作弊 Token</h3>
|
||||||
|
<span id="s1tag" class="tag no">未开始</span>
|
||||||
|
</div>
|
||||||
|
<p class="desc">调用易盾接口获取实时 token,广告接口需要此 token 才能正常响应</p>
|
||||||
|
<pre id="s1pre">(尚未执行)</pre>
|
||||||
|
<div class="row">
|
||||||
|
<button id="step1" class="primary">① 注册 Token</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 步骤 2: 获取广告 -->
|
||||||
|
<div class="block">
|
||||||
|
<div class="step">
|
||||||
|
<div class="num" id="s2num">2</div>
|
||||||
|
<div class="body">
|
||||||
|
<div class="flex sb">
|
||||||
|
<h3>获取广告 & 提取 reqId</h3>
|
||||||
|
<span id="s2tag" class="tag no">未开始</span>
|
||||||
|
</div>
|
||||||
|
<p class="desc">请求广告位,从返回的 extJson.contextInfo.req_id 提取用于领取权益的标识</p>
|
||||||
|
<pre id="s2pre" class="data">(尚未执行)</pre>
|
||||||
|
<div class="row">
|
||||||
|
<button id="step2" class="primary" disabled>② 获取广告</button>
|
||||||
|
<button id="step2b" disabled>换一个广告位</button>
|
||||||
|
</div>
|
||||||
|
<div class="info-grid" id="s2info" style="display:none">
|
||||||
|
<div class="lbl">广告 ID</div><div id="s2adId">-</div>
|
||||||
|
<div class="lbl">reqId</div><div id="s2reqId">-</div>
|
||||||
|
<div class="lbl">文案</div><div id="s2text">-</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 步骤 3: 领取权益 -->
|
||||||
|
<div class="block">
|
||||||
|
<div class="step">
|
||||||
|
<div class="num" id="s3num">3</div>
|
||||||
|
<div class="body">
|
||||||
|
<div class="flex sb">
|
||||||
|
<h3>领取免费听权益</h3>
|
||||||
|
<span id="s3tag" class="tag no">未开始</span>
|
||||||
|
</div>
|
||||||
|
<p class="desc">传入 reqId 及相关参数,领取免费听权益(模拟点击广告后的领取请求)</p>
|
||||||
|
<pre id="s3pre" class="data">(尚未执行)</pre>
|
||||||
|
<div class="row">
|
||||||
|
<button id="step3" class="primary" disabled>③ 领取权益</button>
|
||||||
|
</div>
|
||||||
|
<div class="info-grid" id="s3info" style="display:none">
|
||||||
|
<div class="lbl">结果</div><div id="s3result">-</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 一键全流程 -->
|
||||||
|
<div class="block">
|
||||||
|
<div class="flex sb">
|
||||||
|
<h3>一键运行</h3>
|
||||||
|
<span id="allTag" class="tag no">就绪</span>
|
||||||
|
</div>
|
||||||
|
<p class="desc" style="margin-top:4px">按顺序自动执行以上三个步骤</p>
|
||||||
|
<div class="row">
|
||||||
|
<button id="runAll" class="primary">▶ 一键获取免费听时长</button>
|
||||||
|
<button id="clearAll" class="danger">✕ 清空</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 剩余时长计时器 -->
|
||||||
|
<div class="block" id="rightsBlock">
|
||||||
|
<div class="flex sb">
|
||||||
|
<h3 id="rightsTitleDisplay">🕐 免费听时长剩余</h3>
|
||||||
|
<span id="rightsTag" class="tag no">未查询</span>
|
||||||
|
</div>
|
||||||
|
<p class="desc" id="rightsSubDesc">查询你的免费听权益剩余时长</p>
|
||||||
|
<div class="timer-wrap">
|
||||||
|
<div class="timer-row" id="timerRow">
|
||||||
|
<div class="timer-unit">
|
||||||
|
<span class="timer-num" id="timerDays">--</span>
|
||||||
|
<span class="timer-label">天</span>
|
||||||
|
</div>
|
||||||
|
<span class="timer-sep">:</span>
|
||||||
|
<div class="timer-unit">
|
||||||
|
<span class="timer-num" id="timerHours">--</span>
|
||||||
|
<span class="timer-label">时</span>
|
||||||
|
</div>
|
||||||
|
<span class="timer-sep">:</span>
|
||||||
|
<div class="timer-unit">
|
||||||
|
<span class="timer-num" id="timerMinutes">--</span>
|
||||||
|
<span class="timer-label">分</span>
|
||||||
|
</div>
|
||||||
|
<span class="timer-sep">:</span>
|
||||||
|
<div class="timer-unit">
|
||||||
|
<span class="timer-num" id="timerSeconds">--</span>
|
||||||
|
<span class="timer-label">秒</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row" style="justify-content:center">
|
||||||
|
<button id="queryRightsBtn" class="primary">⟳ 查询剩余时长</button>
|
||||||
|
<button id="claim30Btn">领取30分钟</button>
|
||||||
|
</div>
|
||||||
|
<div class="info-grid" id="rightsInfo" style="display:none">
|
||||||
|
<div class="lbl">状态</div><div id="rightsStatus">-</div>
|
||||||
|
<div class="lbl">结束时间</div><div id="rightsEndTimeDisplay">-</div>
|
||||||
|
<div class="lbl">今日已覆盖</div><div id="rightsCoverToday">-</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer class="site-footer">
|
||||||
|
<a href="/">← 返回首页</a>
|
||||||
|
</footer>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const $ = (id) => document.getElementById(id)
|
||||||
|
const base = location.origin
|
||||||
|
|
||||||
|
function setTag(id, type, text) {
|
||||||
|
const el = $(id)
|
||||||
|
el.className = 'tag ' + type
|
||||||
|
el.textContent = text
|
||||||
|
}
|
||||||
|
|
||||||
|
function setNum(id, done) {
|
||||||
|
$(id).className = 'num' + (done ? ' done' : '')
|
||||||
|
}
|
||||||
|
|
||||||
|
function nocache(url) {
|
||||||
|
const sep = url.indexOf('?') > -1 ? '&' : '?'
|
||||||
|
return url + sep + '_t=' + Date.now()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 步骤 1: 注册 Token =====
|
||||||
|
async function step1() {
|
||||||
|
const btn = $('step1')
|
||||||
|
btn.disabled = true
|
||||||
|
setTag('s1tag', 'busy', '注册中…')
|
||||||
|
setNum('s1num', false)
|
||||||
|
try {
|
||||||
|
const res = await fetch(nocache(base + '/register/checktoken/v3?refresh=1'))
|
||||||
|
const data = await res.json()
|
||||||
|
$('s1pre').textContent = JSON.stringify(data, null, 2)
|
||||||
|
if (data.token) {
|
||||||
|
setTag('s1tag', 'ok', '已注册')
|
||||||
|
setNum('s1num', true)
|
||||||
|
$('step2').disabled = false
|
||||||
|
$('step2b').disabled = false
|
||||||
|
return data.token
|
||||||
|
}
|
||||||
|
setTag('s1tag', 'no', '失败')
|
||||||
|
return null
|
||||||
|
} catch (e) {
|
||||||
|
$('s1pre').textContent = JSON.stringify({ error: e.message }, null, 2)
|
||||||
|
setTag('s1tag', 'no', e.message)
|
||||||
|
return null
|
||||||
|
} finally {
|
||||||
|
btn.disabled = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 步骤 2: 获取广告 =====
|
||||||
|
async function step2(typeId) {
|
||||||
|
const btn = $('step2')
|
||||||
|
const btnB = $('step2b')
|
||||||
|
btn.disabled = btnB.disabled = true
|
||||||
|
setTag('s2tag', 'busy', '获取中…')
|
||||||
|
setNum('s2num', false)
|
||||||
|
$('s2info').style.display = 'none'
|
||||||
|
try {
|
||||||
|
let url = base + '/ad/get' + (typeId ? '?type_ids=' + encodeURIComponent(typeId) : '')
|
||||||
|
const res = await fetch(nocache(url))
|
||||||
|
const data = await res.json()
|
||||||
|
$('s2pre').textContent = JSON.stringify(data, null, 2)
|
||||||
|
|
||||||
|
if (data.code === 200 && data.extra && data.extra.reqId) {
|
||||||
|
setTag('s2tag', 'ok', '已获取')
|
||||||
|
setNum('s2num', true)
|
||||||
|
$('s2info').style.display = 'grid'
|
||||||
|
|
||||||
|
const adKeys = data.ads ? Object.keys(data.ads) : []
|
||||||
|
let adId = '-', text = '-'
|
||||||
|
if (adKeys.length > 0) {
|
||||||
|
const ad = data.ads[adKeys[0]]
|
||||||
|
adId = ad.ad_id || '-'
|
||||||
|
text = ad.text || '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
$('s2adId').textContent = adId
|
||||||
|
$('s2reqId').textContent = data.extra.reqId
|
||||||
|
$('s2text').textContent = text
|
||||||
|
|
||||||
|
$('step3').disabled = false
|
||||||
|
return data.extra.reqId
|
||||||
|
}
|
||||||
|
setTag('s2tag', 'no', '未获取到 reqId')
|
||||||
|
return null
|
||||||
|
} catch (e) {
|
||||||
|
$('s2pre').textContent = JSON.stringify({ error: e.message }, null, 2)
|
||||||
|
setTag('s2tag', 'no', e.message)
|
||||||
|
return null
|
||||||
|
} finally {
|
||||||
|
btn.disabled = false
|
||||||
|
btnB.disabled = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$('step2').onclick = () => step2()
|
||||||
|
$('step2b').onclick = () => step2('["400002_0"]')
|
||||||
|
|
||||||
|
// ===== 步骤 3: 领取权益 =====
|
||||||
|
async function step3() {
|
||||||
|
const btn = $('step3')
|
||||||
|
btn.disabled = true
|
||||||
|
setTag('s3tag', 'busy', '领取中…')
|
||||||
|
setNum('s3num', false)
|
||||||
|
$('s3info').style.display = 'none'
|
||||||
|
try {
|
||||||
|
const res = await fetch(nocache(base + '/ad/listening/rights/gain'))
|
||||||
|
const data = await res.json()
|
||||||
|
$('s3pre').textContent = JSON.stringify(data, null, 2)
|
||||||
|
|
||||||
|
if (data.code === 200) {
|
||||||
|
setTag('s3tag', 'ok', '已领取')
|
||||||
|
setNum('s3num', true)
|
||||||
|
$('s3info').style.display = 'grid'
|
||||||
|
const body = data.data || {}
|
||||||
|
$('s3result').textContent = JSON.stringify(body).substring(0, 200)
|
||||||
|
// 领取成功后自动刷新剩余时长
|
||||||
|
queryRights()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
setTag('s3tag', 'no', '领取失败')
|
||||||
|
return false
|
||||||
|
} catch (e) {
|
||||||
|
$('s3pre').textContent = JSON.stringify({ error: e.message }, null, 2)
|
||||||
|
setTag('s3tag', 'no', e.message)
|
||||||
|
return false
|
||||||
|
} finally {
|
||||||
|
btn.disabled = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$('step3').onclick = step3
|
||||||
|
|
||||||
|
// ===== 一键运行 =====
|
||||||
|
$('runAll').onclick = async function () {
|
||||||
|
const btn = this
|
||||||
|
btn.disabled = true
|
||||||
|
setTag('allTag', 'busy', '运行中…')
|
||||||
|
|
||||||
|
await step1()
|
||||||
|
await step2()
|
||||||
|
await step3()
|
||||||
|
|
||||||
|
setTag('allTag', 'ok', '完成')
|
||||||
|
btn.disabled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 清空 =====
|
||||||
|
$('clearAll').onclick = function () {
|
||||||
|
const steps = ['s1', 's2', 's3']
|
||||||
|
const tags = { s1: '未开始', s2: '未开始', s3: '未开始' }
|
||||||
|
steps.forEach(s => {
|
||||||
|
setTag(s + 'tag', 'no', tags[s])
|
||||||
|
setNum(s + 'num', false)
|
||||||
|
$(s + 'pre').textContent = '(尚未执行)'
|
||||||
|
const info = $(s + 'info')
|
||||||
|
if (info) info.style.display = 'none'
|
||||||
|
})
|
||||||
|
$('step1').disabled = false
|
||||||
|
$('step2').disabled = true
|
||||||
|
$('step2b').disabled = true
|
||||||
|
$('step3').disabled = true
|
||||||
|
setTag('allTag', 'no', '就绪')
|
||||||
|
$('runAll').disabled = false
|
||||||
|
|
||||||
|
// 重置计时器
|
||||||
|
stopCountdown()
|
||||||
|
$('timerDays').textContent = '--'
|
||||||
|
$('timerHours').textContent = '--'
|
||||||
|
$('timerMinutes').textContent = '--'
|
||||||
|
$('timerSeconds').textContent = '--'
|
||||||
|
setTag('rightsTag', 'no', '未查询')
|
||||||
|
$('rightsInfo').style.display = 'none'
|
||||||
|
$('rightsTitleDisplay').textContent = '🕐 免费听时长剩余'
|
||||||
|
$('rightsSubDesc').textContent = '查询你的免费听权益剩余时长'
|
||||||
|
$('runAll').disabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 剩余时长计时器 =====
|
||||||
|
let countdownTimer = null
|
||||||
|
let remainingMs = 0
|
||||||
|
|
||||||
|
/** 毫秒 → { d, h, m, s } */
|
||||||
|
function parseMs(ms) {
|
||||||
|
if (ms <= 0) return { d: 0, h: 0, m: 0, s: 0 }
|
||||||
|
const totalSec = Math.floor(ms / 1000)
|
||||||
|
return {
|
||||||
|
d: Math.floor(totalSec / 86400),
|
||||||
|
h: Math.floor((totalSec % 86400) / 3600),
|
||||||
|
m: Math.floor((totalSec % 3600) / 60),
|
||||||
|
s: totalSec % 60
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTimer(ms) {
|
||||||
|
const t = parseMs(ms)
|
||||||
|
$('timerDays').textContent = String(t.d).padStart(2, '0')
|
||||||
|
$('timerHours').textContent = String(t.h).padStart(2, '0')
|
||||||
|
$('timerMinutes').textContent = String(t.m).padStart(2, '0')
|
||||||
|
$('timerSeconds').textContent = String(t.s).padStart(2, '0')
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopCountdown() {
|
||||||
|
if (countdownTimer) {
|
||||||
|
clearInterval(countdownTimer)
|
||||||
|
countdownTimer = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startCountdown(ms) {
|
||||||
|
stopCountdown()
|
||||||
|
remainingMs = Math.max(0, ms)
|
||||||
|
renderTimer(remainingMs)
|
||||||
|
if (remainingMs <= 0) {
|
||||||
|
setTag('rightsTag', 'no', '已用完')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
countdownTimer = setInterval(() => {
|
||||||
|
remainingMs = Math.max(0, remainingMs - 1000)
|
||||||
|
renderTimer(remainingMs)
|
||||||
|
if (remainingMs <= 0) {
|
||||||
|
stopCountdown()
|
||||||
|
setTag('rightsTag', 'no', '已过期')
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function queryRights() {
|
||||||
|
const btn = $('queryRightsBtn')
|
||||||
|
btn.disabled = true
|
||||||
|
setTag('rightsTag', 'busy', '查询中…')
|
||||||
|
try {
|
||||||
|
const res = await fetch(nocache(base + '/ad/listening/rights'))
|
||||||
|
const data = await res.json()
|
||||||
|
if (data.code === 200 && data.data) {
|
||||||
|
const d = data.data
|
||||||
|
$('rightsTitleDisplay').textContent = '🕐 ' + (d.title || '免费听时长剩余')
|
||||||
|
$('rightsSubDesc').textContent = d.vipInfoContent || '查询你的免费听权益剩余时长'
|
||||||
|
$('rightsInfo').style.display = 'grid'
|
||||||
|
|
||||||
|
// 状态
|
||||||
|
const status = d.status || ''
|
||||||
|
$('rightsStatus').textContent = status
|
||||||
|
|
||||||
|
// 结束时间
|
||||||
|
if (d.rightsEndTime) {
|
||||||
|
const endDate = new Date(d.rightsEndTime)
|
||||||
|
$('rightsEndTimeDisplay').textContent = endDate.toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 今日已覆盖
|
||||||
|
$('rightsCoverToday').textContent = d.rightsCoverToday ? '是 ✓' : '否'
|
||||||
|
|
||||||
|
// 倒计时
|
||||||
|
if (d.rightsRemainingTime > 0) {
|
||||||
|
startCountdown(d.rightsRemainingTime)
|
||||||
|
setTag('rightsTag', 'ok', '已解锁 ✓')
|
||||||
|
} else {
|
||||||
|
renderTimer(0)
|
||||||
|
setTag('rightsTag', 'no', '已用完')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 领取按钮(仅当 step3 可用时启用)
|
||||||
|
if (d.cardContent && d.cardContent.actionTitle && $('step3').disabled === false) {
|
||||||
|
$('claim30Btn').disabled = false
|
||||||
|
$('claim30Btn').textContent = d.cardContent.actionTitle
|
||||||
|
} else {
|
||||||
|
$('claim30Btn').disabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (d.rightsUpperLimit) {
|
||||||
|
$('rightsStatus').textContent += ' (已达上限)'
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.data
|
||||||
|
}
|
||||||
|
setTag('rightsTag', 'no', '查询失败')
|
||||||
|
return null
|
||||||
|
} catch (e) {
|
||||||
|
setTag('rightsTag', 'no', e.message)
|
||||||
|
return null
|
||||||
|
} finally {
|
||||||
|
btn.disabled = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$('queryRightsBtn').onclick = queryRights
|
||||||
|
|
||||||
|
$('claim30Btn').onclick = async function () {
|
||||||
|
const ok = await step3()
|
||||||
|
if (ok) await queryRights()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面加载后自动查询一次
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
setTimeout(queryRights, 300)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+7
-1
@@ -95,7 +95,13 @@ curl -s {origin}/search?keywords=网易云</code></pre>
|
|||||||
<a href="/api_decrypt.html">API 解密</a> ·
|
<a href="/api_decrypt.html">API 解密</a> ·
|
||||||
<a href="/listen_together_host.html">一起听示例</a> ·
|
<a href="/listen_together_host.html">一起听示例</a> ·
|
||||||
<a href="/playlist_cover_update.html">更新歌单封面示例</a> ·
|
<a href="/playlist_cover_update.html">更新歌单封面示例</a> ·
|
||||||
<a href="/avatar_update.html">头像更新示例</a>
|
<a href="/avatar_update.html">头像更新示例</a> ·
|
||||||
|
<a href="/scrobble.html">听歌打卡示例</a> ·
|
||||||
|
<a href="/yidun.html">获取 CheckToken</a> ·
|
||||||
|
<a href="/free_listen.html">免费听权益</a> ·
|
||||||
|
<a href="/yunbei_task.html">云贝广告任务</a> ·
|
||||||
|
<a href="/ugc.html">云小编任务中心</a> ·
|
||||||
|
<a href="/ugc_lottery.html">云小编每日抽奖</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -148,12 +148,12 @@
|
|||||||
updateStatus('二维码已过期,请刷新页面', 'error')
|
updateStatus('二维码已过期,请刷新页面', 'error')
|
||||||
clearInterval(timer)
|
clearInterval(timer)
|
||||||
} else if (statusRes.code === 801) {
|
} else if (statusRes.code === 801) {
|
||||||
updateStatus('二维码已扫描,请在手机上确认', 'waiting')
|
updateStatus('等待手机扫码...', 'waiting')
|
||||||
} else if (statusRes.code === 802) {
|
} else if (statusRes.code === 802) {
|
||||||
updateStatus('登录成功,正在保存信息...', 'waiting')
|
updateStatus('二维码已扫描,请在手机上确认', 'waiting')
|
||||||
} else if (statusRes.code === 803) {
|
} else if (statusRes.code === 803) {
|
||||||
clearInterval(timer)
|
clearInterval(timer)
|
||||||
updateStatus('授权登录成功!', 'success')
|
updateStatus('授权登录成功,正在保存信息...', 'success')
|
||||||
await getLoginStatus(statusRes.cookie)
|
await getLoginStatus(statusRes.cookie)
|
||||||
localStorage.setItem('cookie', statusRes.cookie)
|
localStorage.setItem('cookie', statusRes.cookie)
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -148,12 +148,12 @@
|
|||||||
updateStatus('二维码已过期,请刷新页面', 'error')
|
updateStatus('二维码已过期,请刷新页面', 'error')
|
||||||
clearInterval(timer)
|
clearInterval(timer)
|
||||||
} else if (statusRes.code === 801) {
|
} else if (statusRes.code === 801) {
|
||||||
updateStatus('二维码已扫描,请在手机上确认', 'waiting')
|
updateStatus('等待手机扫码...', 'waiting')
|
||||||
} else if (statusRes.code === 802) {
|
} else if (statusRes.code === 802) {
|
||||||
updateStatus('登录成功,正在保存信息...', 'waiting')
|
updateStatus('二维码已扫描,请在手机上确认', 'waiting')
|
||||||
} else if (statusRes.code === 803) {
|
} else if (statusRes.code === 803) {
|
||||||
clearInterval(timer)
|
clearInterval(timer)
|
||||||
updateStatus('授权登录成功!', 'success')
|
updateStatus('授权登录成功,正在保存信息...', 'success')
|
||||||
await getLoginStatus(statusRes.cookie)
|
await getLoginStatus(statusRes.cookie)
|
||||||
localStorage.setItem('cookie', statusRes.cookie)
|
localStorage.setItem('cookie', statusRes.cookie)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,480 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>听歌打卡 - 网易云音乐 API Enhanced</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f5f5f5;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 520px;
|
||||||
|
margin: 40px auto;
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 32px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-link {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
text-decoration: none;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-link:hover {
|
||||||
|
color: #333;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Tabs ---- */
|
||||||
|
.tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 0;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
border-bottom: 2px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-btn {
|
||||||
|
flex: 1;
|
||||||
|
padding: 10px 0;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
margin-bottom: -2px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #999;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.2s, border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-btn:hover {
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-btn.active {
|
||||||
|
color: #333;
|
||||||
|
border-bottom-color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content.active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Form ---- */
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #555;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
label .tag {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 400;
|
||||||
|
padding: 1px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
margin-left: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
label .tag.r {
|
||||||
|
background: #fee2e2;
|
||||||
|
color: #991b1b;
|
||||||
|
}
|
||||||
|
|
||||||
|
label .tag.o {
|
||||||
|
background: #fef3c7;
|
||||||
|
color: #92400e;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"],
|
||||||
|
input[type="number"],
|
||||||
|
textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 14px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
outline: none;
|
||||||
|
font-family: inherit;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"]:focus,
|
||||||
|
input[type="number"]:focus,
|
||||||
|
textarea:focus {
|
||||||
|
border-color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .form-group {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-fill {
|
||||||
|
margin-top: 5px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-fill span {
|
||||||
|
color: #0066cc;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-fill span:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
background: #333;
|
||||||
|
color: white;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
border: none;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
background: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:disabled {
|
||||||
|
background: #ccc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result {
|
||||||
|
margin-top: 16px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
text-align: left;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result.success {
|
||||||
|
background: #d1fae5;
|
||||||
|
color: #065f46;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result.error {
|
||||||
|
background: #fee2e2;
|
||||||
|
color: #991b1b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result.info {
|
||||||
|
background: #e0f2fe;
|
||||||
|
color: #0369a1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-link {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 24px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-link a {
|
||||||
|
color: #666;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-link a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>听歌打卡</h1>
|
||||||
|
<p class="subtitle">同步听歌记录至网易云音乐,增加听歌排行计数</p>
|
||||||
|
|
||||||
|
<a href="/qrlogin-nocookie.html" class="login-link">还没登录?点击登录</a>
|
||||||
|
|
||||||
|
<!-- Cookie (共享) -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="cookie">Cookie <span class="tag o">可选</span></label>
|
||||||
|
<textarea id="cookie" placeholder="留空则默认读取本地存储的登录态" rows="2"></textarea>
|
||||||
|
<div class="quick-fill">
|
||||||
|
<span onclick="loadLocalCookie()">读取本地 Cookie</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tabs -->
|
||||||
|
<div class="tabs">
|
||||||
|
<button class="tab-btn active" data-tab="v0" onclick="switchTab('v0')">原版</button>
|
||||||
|
<button class="tab-btn" data-tab="v1" onclick="switchTab('v1')">V2 (NCBL 加密版)</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ============ Tab: v0 (原版 /scrobble) ============ -->
|
||||||
|
<div class="tab-content active" id="tab-v0">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>歌曲 ID <span class="tag r">必填</span></label>
|
||||||
|
<input type="text" id="v0-songId" placeholder="例如: 2756058128" value="2756058128" />
|
||||||
|
<div class="quick-fill">
|
||||||
|
示例:
|
||||||
|
<span onclick="fillV0('2756058128', '288651229')">妖精小姐的魔法邀约</span>
|
||||||
|
<span onclick="fillV0('2637402867', '251025018')">Echoes of Memoria</span>
|
||||||
|
<span onclick="fillV0('36307815', '3394198')">Lose Control</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>来源 ID (歌单或专辑 ID) <span class="tag r">必填</span></label>
|
||||||
|
<input type="text" id="v0-sourceId" placeholder="例如: 2756058128" value="2756058128" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>播放时间 (秒) <span class="tag o">可选</span></label>
|
||||||
|
<input type="number" id="v0-time" placeholder="300" value="300" />
|
||||||
|
</div>
|
||||||
|
<button class="btn" onclick="submitV0()">立即打卡 (原版)</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ============ Tab: v1 (NCBL 加密版 /scrobble/v1) ============ -->
|
||||||
|
<div class="tab-content" id="tab-v1">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>歌曲 ID <span class="tag r">必填</span></label>
|
||||||
|
<input type="text" id="v1-songId" placeholder="例如: 518066366" value="518066366" />
|
||||||
|
<div class="quick-fill">
|
||||||
|
示例:
|
||||||
|
<span onclick="fillV1('518066366', '36780169', '291')">默认示例</span>
|
||||||
|
<span onclick="fillV1('2756058128', '288651229', '300')">妖精小姐的魔法邀约</span>
|
||||||
|
<span onclick="fillV1('2637402867', '251025018', '300')">Echoes of Memoria</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>播放时间 (秒) <span class="tag r">必填</span></label>
|
||||||
|
<input type="number" id="v1-time" placeholder="例如: 291" value="291" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>来源 ID <span class="tag o">可选</span></label>
|
||||||
|
<input type="text" id="v1-sourceId" placeholder="歌单或专辑 ID" value="36780169" />
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>歌曲名 <span class="tag o">可选</span></label>
|
||||||
|
<input type="text" id="v1-name" placeholder="歌曲名称" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>艺术家 <span class="tag o">可选</span></label>
|
||||||
|
<input type="text" id="v1-artist" placeholder="歌手名" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>码率 <span class="tag o">可选</span></label>
|
||||||
|
<input type="number" id="v1-bitrate" placeholder="默认 320" value="320" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>音质等级 <span class="tag o">可选</span></label>
|
||||||
|
<input type="text" id="v1-level" placeholder="默认 exhigh" value="exhigh" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>歌曲总时长 (秒) <span class="tag o">可选</span></label>
|
||||||
|
<input type="number" id="v1-total" placeholder="留空则与播放时间相同" />
|
||||||
|
</div>
|
||||||
|
<button class="btn" onclick="submitV1()">立即打卡 (NCBL 加密版)</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Result -->
|
||||||
|
<div id="result" class="result" style="display: none;"></div>
|
||||||
|
|
||||||
|
<div class="footer-link">
|
||||||
|
<a href="/">返回首页</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://fastly.jsdelivr.net/npm/[email protected]/dist/axios.min.js"></script>
|
||||||
|
<script>
|
||||||
|
/* ---- DOM refs ---- */
|
||||||
|
const cookieInput = document.getElementById('cookie')
|
||||||
|
const resultDiv = document.getElementById('result')
|
||||||
|
|
||||||
|
/* ---- init ---- */
|
||||||
|
if (localStorage.getItem('cookie')) {
|
||||||
|
cookieInput.value = localStorage.getItem('cookie')
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Tab switching ---- */
|
||||||
|
function switchTab(name) {
|
||||||
|
document.querySelectorAll('.tab-btn').forEach(b => b.classList.toggle('active', b.dataset.tab === name))
|
||||||
|
document.querySelectorAll('.tab-content').forEach(c => c.classList.toggle('active', c.id === 'tab-' + name))
|
||||||
|
hideResult()
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Cookie ---- */
|
||||||
|
function loadLocalCookie() {
|
||||||
|
const local = localStorage.getItem('cookie')
|
||||||
|
if (local) {
|
||||||
|
cookieInput.value = local
|
||||||
|
showResult('已成功读取本地 Cookie', 'success')
|
||||||
|
} else {
|
||||||
|
showResult('未在本地发现登录 Cookie,请先登录或手动粘贴', 'error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Quick fill ---- */
|
||||||
|
function fillV0(id, sourceId) {
|
||||||
|
document.getElementById('v0-songId').value = id
|
||||||
|
document.getElementById('v0-sourceId').value = sourceId
|
||||||
|
}
|
||||||
|
|
||||||
|
function fillV1(id, sourceId, time) {
|
||||||
|
document.getElementById('v1-songId').value = id
|
||||||
|
document.getElementById('v1-sourceId').value = sourceId
|
||||||
|
document.getElementById('v1-time').value = time
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Result ---- */
|
||||||
|
function hideResult() {
|
||||||
|
resultDiv.style.display = 'none'
|
||||||
|
}
|
||||||
|
|
||||||
|
function showResult(message, type) {
|
||||||
|
resultDiv.textContent = message
|
||||||
|
resultDiv.className = 'result ' + type
|
||||||
|
resultDiv.style.display = 'block'
|
||||||
|
}
|
||||||
|
|
||||||
|
function disableAll(disabled) {
|
||||||
|
document.querySelectorAll('.btn').forEach(b => b.disabled = disabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Submit: 原版 /scrobble ---- */
|
||||||
|
async function submitV0() {
|
||||||
|
const id = document.getElementById('v0-songId').value.trim()
|
||||||
|
const sourceid = document.getElementById('v0-sourceId').value.trim()
|
||||||
|
const time = document.getElementById('v0-time').value.trim() || '300'
|
||||||
|
|
||||||
|
if (!id) { showResult('请输入歌曲 ID !', 'error'); return }
|
||||||
|
if (!sourceid) { showResult('请输入来源 ID !', 'error'); return }
|
||||||
|
|
||||||
|
disableAll(true)
|
||||||
|
showResult('打卡中,请稍候...', 'info')
|
||||||
|
|
||||||
|
try {
|
||||||
|
const params = { id, sourceid: sourceid || id, time, timestamp: Date.now() }
|
||||||
|
const cookie = cookieInput.value.trim()
|
||||||
|
if (cookie) params.cookie = cookie
|
||||||
|
|
||||||
|
const res = await axios({ method: 'get', url: '/scrobble', params })
|
||||||
|
if (res.data.code === 200) {
|
||||||
|
showResult('✅ 打卡成功! \n\n' + JSON.stringify(res.data, null, 2), 'success')
|
||||||
|
} else {
|
||||||
|
showResult('请求已发送,返回:\n\n' + JSON.stringify(res.data, null, 2), 'info')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const errData = error.response ? error.response.data : { error: error.message }
|
||||||
|
showResult('❌ 打卡失败\n\n' + JSON.stringify(errData, null, 2), 'error')
|
||||||
|
} finally {
|
||||||
|
disableAll(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Submit: NCBL 加密版 /scrobble/v1 ---- */
|
||||||
|
async function submitV1() {
|
||||||
|
const id = document.getElementById('v1-songId').value.trim()
|
||||||
|
const time = document.getElementById('v1-time').value.trim()
|
||||||
|
|
||||||
|
if (!id) { showResult('请输入歌曲 ID !', 'error'); return }
|
||||||
|
if (!time || isNaN(+time) || +time <= 0) { showResult('请输入有效的播放时长(秒)!', 'error'); return }
|
||||||
|
|
||||||
|
disableAll(true)
|
||||||
|
showResult('打卡中(NCBL 加密),请稍候...', 'info')
|
||||||
|
|
||||||
|
try {
|
||||||
|
const params = {
|
||||||
|
id,
|
||||||
|
time,
|
||||||
|
sourceid: document.getElementById('v1-sourceId').value.trim() || undefined,
|
||||||
|
name: document.getElementById('v1-name').value.trim() || undefined,
|
||||||
|
artist: document.getElementById('v1-artist').value.trim() || undefined,
|
||||||
|
bitrate: document.getElementById('v1-bitrate').value.trim() || undefined,
|
||||||
|
level: document.getElementById('v1-level').value.trim() || undefined,
|
||||||
|
total: document.getElementById('v1-total').value.trim() || undefined,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
}
|
||||||
|
const cookie = cookieInput.value.trim()
|
||||||
|
if (cookie) params.cookie = cookie
|
||||||
|
|
||||||
|
// 清理 undefined 参数
|
||||||
|
Object.keys(params).forEach(k => params[k] === undefined && delete params[k])
|
||||||
|
|
||||||
|
const res = await axios({ method: 'get', url: '/scrobble/v1', params })
|
||||||
|
if (res.data.code === 200) {
|
||||||
|
showResult('✅ NCBL 加密打卡成功! \n\n' + JSON.stringify(res.data, null, 2), 'success')
|
||||||
|
} else {
|
||||||
|
showResult('请求已发送,返回:\n\n' + JSON.stringify(res.data, null, 2), 'info')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const errData = error.response ? error.response.data : { error: error.message }
|
||||||
|
showResult('❌ 打卡失败\n\n' + JSON.stringify(errData, null, 2), 'error')
|
||||||
|
} finally {
|
||||||
|
disableAll(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
+882
@@ -0,0 +1,882 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta name="referrer" content="no-referrer" />
|
||||||
|
<link rel="icon" href="docs/netease.png" />
|
||||||
|
<meta name="referrer" content="no-referrer" />
|
||||||
|
<link rel="icon" href="docs/netease.png" />
|
||||||
|
<title>云小编任务中心 - 网易云音乐 API Enhanced</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f5f5f5;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 40px auto;
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 32px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-link {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
text-decoration: none;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-link:hover {
|
||||||
|
color: #333;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Section ---- */
|
||||||
|
.section {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section:last-of-type {
|
||||||
|
border-bottom: none;
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Form ---- */
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #555;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
label .tag {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 400;
|
||||||
|
padding: 1px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
margin-left: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
label .tag.r {
|
||||||
|
background: #fee2e2;
|
||||||
|
color: #991b1b;
|
||||||
|
}
|
||||||
|
|
||||||
|
label .tag.o {
|
||||||
|
background: #fef3c7;
|
||||||
|
color: #92400e;
|
||||||
|
}
|
||||||
|
|
||||||
|
label .tag.g {
|
||||||
|
background: #d1fae5;
|
||||||
|
color: #065f46;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"],
|
||||||
|
input[type="number"],
|
||||||
|
textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 14px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
outline: none;
|
||||||
|
font-family: inherit;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"]:focus,
|
||||||
|
input[type="number"]:focus,
|
||||||
|
textarea:focus {
|
||||||
|
border-color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .form-group {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-fill {
|
||||||
|
margin-top: 5px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-fill span {
|
||||||
|
color: #0066cc;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-fill span:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
background: #333;
|
||||||
|
color: white;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
border: none;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
background: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:disabled {
|
||||||
|
background: #ccc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm {
|
||||||
|
display: inline-block;
|
||||||
|
width: auto;
|
||||||
|
padding: 6px 16px;
|
||||||
|
background: #333;
|
||||||
|
color: white;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
border: none;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm:hover {
|
||||||
|
background: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm:disabled {
|
||||||
|
background: #ccc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm.green {
|
||||||
|
background: #059669;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm.green:hover {
|
||||||
|
background: #047857;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm.green:disabled {
|
||||||
|
background: #a7f3d0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm.red {
|
||||||
|
background: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm.red:hover {
|
||||||
|
background: #b91c1c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm.red:disabled {
|
||||||
|
background: #fca5a5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm.gray {
|
||||||
|
background: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm.gray:hover {
|
||||||
|
background: #4b5563;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm.gray:disabled {
|
||||||
|
background: #d1d5db;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result {
|
||||||
|
margin-top: 16px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
text-align: left;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result.success {
|
||||||
|
background: #d1fae5;
|
||||||
|
color: #065f46;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result.error {
|
||||||
|
background: #fee2e2;
|
||||||
|
color: #991b1b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result.info {
|
||||||
|
background: #e0f2fe;
|
||||||
|
color: #0369a1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-link {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 24px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-link a {
|
||||||
|
color: #666;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-link a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Info Card ---- */
|
||||||
|
.info-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
background: #f9fafb;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card .avatar {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
border-radius: 50%;
|
||||||
|
object-fit: cover;
|
||||||
|
background: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card .info-text {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card .btn-sm {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#userPoint {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Song Review ---- */
|
||||||
|
.song-review {
|
||||||
|
margin-top: 16px;
|
||||||
|
padding: 16px;
|
||||||
|
background: #f9fafb;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.song-review .cover {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
border-radius: 6px;
|
||||||
|
object-fit: cover;
|
||||||
|
background: #e5e7eb;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.song-review .song-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.song-review audio {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.song-review .song-tag {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.song-review .btn-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.song-review .lyric {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #555;
|
||||||
|
background: #fff;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
max-height: 160px;
|
||||||
|
overflow-y: auto;
|
||||||
|
font-family: monospace;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-text {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>云小编任务中心</h1>
|
||||||
|
<p class="subtitle">管理云音乐审核任务、签到与会员权益领取</p>
|
||||||
|
|
||||||
|
<a href="/qrlogin-nocookie.html" class="login-link">还没登录?点击登录</a>
|
||||||
|
|
||||||
|
<!-- Cookie -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="cookie">Cookie <span class="tag o">可选</span></label>
|
||||||
|
<textarea id="cookie" placeholder="留空则默认读取本地存储的登录态" rows="2"></textarea>
|
||||||
|
<div class="quick-fill">
|
||||||
|
<span onclick="loadLocalCookie()">读取本地 Cookie</span>
|
||||||
|
<span onclick="clearCookie()">清除 Cookie</span>
|
||||||
|
<span onclick="saveCookie()">保存 Cookie</span>
|
||||||
|
<span onclick="loadAll()">一键载入</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-header">
|
||||||
|
<span class="section-title">用户信息</span>
|
||||||
|
<button class="btn-sm" id="userGet">获取用户详情</button>
|
||||||
|
</div>
|
||||||
|
<div class="info-card" id="user">
|
||||||
|
<img src="docs/netease.png" alt="头像" class="avatar" id="userAvatar" />
|
||||||
|
<span class="info-text" id="userName">未登录</span>
|
||||||
|
<span id="userPoint">0</span>
|
||||||
|
<button class="btn-sm green" id="userSign" disabled>签到</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-header">
|
||||||
|
<span class="section-title">会员领取</span>
|
||||||
|
<button class="btn-sm" id="vipGet">获取会员详情</button>
|
||||||
|
</div>
|
||||||
|
<div class="info-card" id="vip">
|
||||||
|
<span class="info-text" id="vipStatus">未载入</span>
|
||||||
|
<button class="btn-sm" id="vipCollect" disabled>领取</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-header">
|
||||||
|
<span class="section-title">审核任务</span>
|
||||||
|
<button class="btn-sm" id="taskGet">获取任务详情</button>
|
||||||
|
</div>
|
||||||
|
<div class="info-card" id="task">
|
||||||
|
<span class="info-text" id="taskInfo">[<span id="taskCount">0</span>/<span id="taskGoal">0</span>] 审核歌曲赚贡献分 (+<span id="taskPoint">0</span>)</span>
|
||||||
|
<button class="btn-sm" id="taskStart" disabled>去完成</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Song Review -->
|
||||||
|
<div class="song-review" id="song" style="display: none;">
|
||||||
|
<img alt="封面" class="cover" id="songCover" />
|
||||||
|
<div class="song-title" id="songTitle">未载入</div>
|
||||||
|
<audio controls id="songAudio"></audio>
|
||||||
|
<div class="song-tag" id="songTag"></div>
|
||||||
|
<div class="btn-group">
|
||||||
|
<button class="btn-sm green" id="songApprove" disabled>同意</button>
|
||||||
|
<button class="btn-sm red" id="songReject" disabled>否决</button>
|
||||||
|
<button class="btn-sm gray" id="songSkip" disabled>跳过 (不计次数)</button>
|
||||||
|
</div>
|
||||||
|
<div class="lyric" id="songLyric">[00:00.00] 暂无歌词</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="result" class="result" style="display: none;"></div>
|
||||||
|
|
||||||
|
<div class="footer-link">
|
||||||
|
<a href="/">返回首页</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var k_cookie = 'ncm_cookie';
|
||||||
|
var cookie = localStorage.getItem(k_cookie) || '';
|
||||||
|
|
||||||
|
/* ---- DOM refs ---- */
|
||||||
|
var cookieInput = document.getElementById('cookie');
|
||||||
|
var resultDiv = document.getElementById('result');
|
||||||
|
|
||||||
|
/* ---- Init ---- */
|
||||||
|
if (cookie) {
|
||||||
|
cookieInput.value = cookie;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Cookie ---- */
|
||||||
|
window.loadLocalCookie = function () {
|
||||||
|
var local = localStorage.getItem(k_cookie);
|
||||||
|
if (local) {
|
||||||
|
cookie = local;
|
||||||
|
cookieInput.value = local;
|
||||||
|
showResult('已成功读取本地 Cookie', 'success');
|
||||||
|
} else {
|
||||||
|
showResult('未在本地发现登录 Cookie,请先登录或手动粘贴', 'error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.clearCookie = function () {
|
||||||
|
cookie = '';
|
||||||
|
cookieInput.value = '';
|
||||||
|
localStorage.removeItem(k_cookie);
|
||||||
|
showResult('已清除本地 Cookie', 'info');
|
||||||
|
};
|
||||||
|
|
||||||
|
window.saveCookie = function () {
|
||||||
|
var ck = getCookie()
|
||||||
|
if (ck) localStorage.setItem(k_cookie, ck)
|
||||||
|
showResult('已保存本地 Cookie', 'info');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Result ---- */
|
||||||
|
function hideResult() {
|
||||||
|
resultDiv.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
function showResult(message, type) {
|
||||||
|
resultDiv.textContent = message;
|
||||||
|
resultDiv.className = 'result ' + type;
|
||||||
|
resultDiv.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCookie() {
|
||||||
|
var ck = cookieInput.value.trim();
|
||||||
|
if (ck) cookie = ck;
|
||||||
|
return ck || cookie || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发起请求 (兼容旧版)
|
||||||
|
* @param {string} url
|
||||||
|
* @param {Record<string,string>} params
|
||||||
|
* @param {(resp:Record<string,any>)=>void} done
|
||||||
|
*/
|
||||||
|
function request(url, params, done) {
|
||||||
|
var xhr = new XMLHttpRequest();
|
||||||
|
xhr.responseType = 'json';
|
||||||
|
xhr.timeout = 1e4;
|
||||||
|
xhr.onload = function () {
|
||||||
|
var r = xhr.response;
|
||||||
|
if (r.code !== 200) {
|
||||||
|
showResult('错误 ' + r.code + ': ' + (r.message || '未知错误'), 'error');
|
||||||
|
} else {
|
||||||
|
done(r);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
xhr.onerror = function () {
|
||||||
|
showResult('网络请求失败: ' + xhr.statusText, 'error');
|
||||||
|
};
|
||||||
|
var ck = getCookie();
|
||||||
|
if (ck) params.cookie = ck;
|
||||||
|
var s = '?_=' + Date.now();
|
||||||
|
for (var k in params) {
|
||||||
|
s += '&' + k + '=' + encodeURIComponent(params[k]);
|
||||||
|
}
|
||||||
|
xhr.open('GET', url + s);
|
||||||
|
xhr.send();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 用户信息 ///
|
||||||
|
var userAvatar = document.getElementById('userAvatar');
|
||||||
|
var userName = document.getElementById('userName');
|
||||||
|
var userPoint = document.getElementById('userPoint');
|
||||||
|
var userSign = document.getElementById('userSign');
|
||||||
|
var userGet = document.getElementById('userGet');
|
||||||
|
|
||||||
|
userGet.onclick = function () {
|
||||||
|
request('rep/ugc/user/get', {}, function (res) {
|
||||||
|
var d = res.data;
|
||||||
|
if (d.imgUrl) userAvatar.src = d.imgUrl + '?param=50y50';
|
||||||
|
userName.textContent = d.name || '游客' + d.id;
|
||||||
|
userPoint.textContent = d.availablePoints;
|
||||||
|
userSign.disabled = d.signed;
|
||||||
|
showResult('用户信息加载成功', 'success');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
userSign.onclick = function () {
|
||||||
|
if (!userSign.disabled) {
|
||||||
|
userSign.disabled = true;
|
||||||
|
request('rep/ugc/user/sign', {}, function (res) {
|
||||||
|
var d = res.data;
|
||||||
|
userSign.disabled = d;
|
||||||
|
showResult('签到' + (d ? '成功 ✓' : '失败 ✗'), d ? 'success' : 'error');
|
||||||
|
if (d) userGet.click();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 会员领取 ///
|
||||||
|
var vipStatus = document.getElementById('vipStatus');
|
||||||
|
var vipCollect = document.getElementById('vipCollect');
|
||||||
|
var vipGet = document.getElementById('vipGet');
|
||||||
|
|
||||||
|
vipGet.onclick = function () {
|
||||||
|
request('rep/ugc/user/vip', {}, function (res) {
|
||||||
|
var d = res.data;
|
||||||
|
vipStatus.textContent = d.status + ': ' + d.title;
|
||||||
|
vipCollect.disabled = d.status !== 20;
|
||||||
|
showResult('会员详情加载成功', 'success');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
vipCollect.onclick = function () {
|
||||||
|
if (!vipCollect.disabled) {
|
||||||
|
vipCollect.disabled = true;
|
||||||
|
request('rep/ugc/user/collect-vip', {}, function (res) {
|
||||||
|
var d = res.data;
|
||||||
|
vipCollect.disabled = d;
|
||||||
|
showResult('领取' + (d ? '成功 ✓' : '失败 ✗'), d ? 'success' : 'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 审核任务 ///
|
||||||
|
var taskCount = document.getElementById('taskCount');
|
||||||
|
var taskGoal = document.getElementById('taskGoal');
|
||||||
|
var taskPoint = document.getElementById('taskPoint');
|
||||||
|
var taskStartBtn = document.getElementById('taskStart');
|
||||||
|
var taskGetBtn = document.getElementById('taskGet');
|
||||||
|
|
||||||
|
taskGetBtn.onclick = function () {
|
||||||
|
request('rep/ugc/activity/get', {}, function (res) {
|
||||||
|
var d = res.data;
|
||||||
|
taskCount.textContent = d.count;
|
||||||
|
taskGoal.textContent = d.goalCount;
|
||||||
|
taskPoint.textContent = d.points;
|
||||||
|
// 注:超过 100 可能有 bug,导致 thinktank/audit/resource/detail 获取的任务 ID 一直是 5002,目标数量也不对,暂时限制前两个任务
|
||||||
|
taskStartBtn.disabled = d.activityId !== 5001 && d.activityId !== 5002; // 5001: 1-50, 5002: 51-100
|
||||||
|
showResult('任务详情加载成功', 'success');
|
||||||
|
// 任务完成,领取积分
|
||||||
|
if (d.count >= d.goalCount) {
|
||||||
|
request('rep/ugc/activity/collect', { activityId: d.activityId }, function (res) {
|
||||||
|
var d = res.data;
|
||||||
|
showResult('积分领取' + (d ? '成功,可以兑换会员了!' : '失败'), d ? 'success' : 'error');
|
||||||
|
// 刷新任务状态
|
||||||
|
if (d) loadAll();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
taskStartBtn.onclick = function () {
|
||||||
|
if (!taskStartBtn.disabled) {
|
||||||
|
songAudio.pause();
|
||||||
|
examInit();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 歌曲审核区域 ///
|
||||||
|
var song = document.getElementById('song');
|
||||||
|
var songCover = document.getElementById('songCover');
|
||||||
|
var songTitle = document.getElementById('songTitle');
|
||||||
|
var songAudio = document.getElementById('songAudio');
|
||||||
|
var songTag = document.getElementById('songTag');
|
||||||
|
var songApprove = document.getElementById('songApprove');
|
||||||
|
var songReject = document.getElementById('songReject');
|
||||||
|
var songSkip = document.getElementById('songSkip');
|
||||||
|
var songLyric = document.getElementById('songLyric');
|
||||||
|
|
||||||
|
var examType = 'emotionEnter';
|
||||||
|
// 考试初始化、检查状态
|
||||||
|
function examInit() {
|
||||||
|
request('rep/ugc/exam/info/get', { examType: examType }, function (res) {
|
||||||
|
var d = res.data;
|
||||||
|
switch (d.process) {
|
||||||
|
case 'whole_exam_end':
|
||||||
|
// 通过考试
|
||||||
|
if (d.hasPassExamination) {
|
||||||
|
taskStart();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
request('rep/ugc/exam/result/get', { examType: examType, taskId: d.taskId }, function (res) {
|
||||||
|
var d = res.data;
|
||||||
|
showResult('考试未通过:正确数:' + d.wrightNum + ',正确率:' + d.wrightRate, 'error');
|
||||||
|
});
|
||||||
|
case 'no':
|
||||||
|
// 未通过或未开始
|
||||||
|
request('rep/ugc/exam/start', { examType: examType }, function (res) {
|
||||||
|
examStart(res.data.taskId);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'process':
|
||||||
|
// 正在考试
|
||||||
|
examStart(d.taskId);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
showResult('未知考试进度:' + d.process, 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 在上个信息不为异常时显示考试中提示
|
||||||
|
if (d.process !== 'whole_exam_end' && !resultDiv.className.endsWith('error')) showResult('正在考试,请认真审题!', 'info');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// 考试取题开始
|
||||||
|
function examStart(taskId) {
|
||||||
|
request('rep/ugc/exam/question/single/get', { examType: examType, taskId: taskId }, function (res) {
|
||||||
|
var d = res.data;
|
||||||
|
|
||||||
|
songCover.src = d.coverUrl + '?param=100y100';
|
||||||
|
songTitle.textContent = '[' + d.resId + '] ' + d.resName + ' - ' + d.artists;
|
||||||
|
songTag.textContent = '[' + (d.auditCount + 1) + '/' + d.auditTaskCount + '] 当前结果: ' + d.questionContent + '?';
|
||||||
|
songLyric.textContent = d.lyric ? d.transLyric ? lrctran(d.lyric, d.transLyric) : d.lyric : '[00:00.00] 暂无歌词';
|
||||||
|
songLyric.scrollTop = 0;
|
||||||
|
// 有时候就是没有播放链接,只能根据歌词硬审
|
||||||
|
if (d.songUrl) {
|
||||||
|
songAudio.src = d.songUrl;
|
||||||
|
songAudio.play().catch(function () { /* 自动播放被浏览器阻止 */ });
|
||||||
|
} else {
|
||||||
|
songAudio.src = ''; // 清理播放器
|
||||||
|
}
|
||||||
|
songApprove.onclick = function () { if (!songApprove.disabled) examSubmit(taskId, d.questionId, 'A') };
|
||||||
|
songReject.onclick = function () { if (!songReject.disabled) examSubmit(taskId, d.questionId, 'B') };
|
||||||
|
songSkip.onclick = null;
|
||||||
|
|
||||||
|
songApprove.disabled = songReject.disabled = songSkip.disabled = true;
|
||||||
|
song.style.display = 'block';
|
||||||
|
|
||||||
|
setTimeout(function () {
|
||||||
|
songApprove.disabled = songReject.disabled = false;
|
||||||
|
}, 3e3);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// 考试提交
|
||||||
|
function examSubmit(taskId, questionId, answer) {
|
||||||
|
songAudio.pause();
|
||||||
|
songApprove.disabled = songReject.disabled = songSkip.disabled = true;
|
||||||
|
request(
|
||||||
|
'rep/ugc/exam/submit',
|
||||||
|
{ examType: examType, taskId: taskId, questionId: questionId, answer: answer },
|
||||||
|
function (res) {
|
||||||
|
var d = res.data;
|
||||||
|
if (d.result) {
|
||||||
|
showResult('回答正确,加载下一首...', 'success');
|
||||||
|
} else {
|
||||||
|
showResult('回答错误:' + d.analysis, 'error');
|
||||||
|
}
|
||||||
|
examInit();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取任务
|
||||||
|
*/
|
||||||
|
function taskStart() {
|
||||||
|
request('thinktank/audit/resource/detail', { type: '4' }, function (res) {
|
||||||
|
var d = res.data;
|
||||||
|
|
||||||
|
if (d.auditCount >= 50) {
|
||||||
|
request('rep/ugc/activity/collect', { activityId: '5001' }, function (res) {
|
||||||
|
var d = res.data;
|
||||||
|
showResult('积分领取' + (d ? '成功,可以兑换会员了!' : '失败'), d ? 'success' : 'error');
|
||||||
|
// 刷新任务状态
|
||||||
|
if (d) {
|
||||||
|
vipGet.click();
|
||||||
|
taskGetBtn.click();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
songAudio.pause()
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
songCover.src = d.coverUrl + '?param=100y100';
|
||||||
|
songTitle.textContent = '[' + d.resId + '] ' + d.resName + ' - ' + d.artists;
|
||||||
|
songTag.textContent = '[' + (d.auditCount + 1) + '/' + d.auditTaskCount + '] 当前结果: ' + d.initResult + '?';
|
||||||
|
songLyric.textContent = d.lyric ? d.transLyric ? lrctran(d.lyric, d.transLyric) : d.lyric : '[00:00.00] 暂无歌词';
|
||||||
|
songLyric.scrollTop = 0;
|
||||||
|
songAudio.src = d.songUrl;
|
||||||
|
songAudio.play().catch(function () { /* 自动播放被浏览器阻止 */ });
|
||||||
|
|
||||||
|
songApprove.onclick = function () { if (!songApprove.disabled) taskSubmit(d.taskId, '1'); };
|
||||||
|
songReject.onclick = function () { if (!songReject.disabled) taskSubmit(d.taskId, '2'); };
|
||||||
|
songSkip.onclick = function () { if (!songSkip.disabled) taskSubmit(d.taskId, '3'); };
|
||||||
|
|
||||||
|
songApprove.disabled = songReject.disabled = songSkip.disabled = true;
|
||||||
|
song.style.display = 'block';
|
||||||
|
|
||||||
|
setTimeout(function () {
|
||||||
|
songApprove.disabled = songReject.disabled = songSkip.disabled = false;
|
||||||
|
}, 3e3);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提交任务
|
||||||
|
* @param {string} taskId
|
||||||
|
* @param {'1'|'2'|'3'} judgement
|
||||||
|
*/
|
||||||
|
function taskSubmit(taskId, judgement) {
|
||||||
|
songAudio.pause();
|
||||||
|
songApprove.disabled = songReject.disabled = songSkip.disabled = true;
|
||||||
|
request(
|
||||||
|
'thinktank/audit/resource/update',
|
||||||
|
{ type: '4', taskId: taskId, judgement: judgement },
|
||||||
|
function (res) {
|
||||||
|
var d = res.data;
|
||||||
|
if (d.result) {
|
||||||
|
showResult((d.test ? '抽查' : '审核') + '提交成功,加载下一首...', 'success');
|
||||||
|
taskStart();
|
||||||
|
} else if (d.test) {
|
||||||
|
showResult('抽查失败: [' + d.dayErrorNum + '/' + d.dayErrorNumStandard + '] ' + d.errorAnalysis, 'error');
|
||||||
|
taskStart();
|
||||||
|
} else {
|
||||||
|
showResult('未知状态: ' + JSON.stringify(d), 'error');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**解析歌词时间戳和文本 */
|
||||||
|
function lrctrim(lyrics) {
|
||||||
|
var lines = lyrics.split('\n');
|
||||||
|
var data = [];
|
||||||
|
lines.forEach(function (line, index) {
|
||||||
|
var matches = line.match(/\[(\d{2}):(\d{2}[\.:]?\d*)\]/);
|
||||||
|
if (matches) {
|
||||||
|
var minutes = parseInt(matches[1], 10);
|
||||||
|
var seconds = parseFloat(matches[2].replace('.', ':')) || 0;
|
||||||
|
var timestamp = minutes * 6e4 + seconds * 1e3;
|
||||||
|
var text = line.replace(/\[\d{2}:\d{2}[\.:]?\d*\]/g, '').trim();
|
||||||
|
text = text.replace(/\s\s+/g, ' '); // Replace multiple spaces with a single space
|
||||||
|
data.push([timestamp, index, text]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
data.sort(function (a, b) {
|
||||||
|
return a[0] - b[0];
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
/**合并原文歌词和翻译歌词 */
|
||||||
|
function lrctran(lyric, tlyric) {
|
||||||
|
lyric = lrctrim(lyric);
|
||||||
|
tlyric = lrctrim(tlyric);
|
||||||
|
var len1 = lyric.length;
|
||||||
|
var len2 = tlyric.length;
|
||||||
|
var result = '';
|
||||||
|
for (var i = 0, j = 0; i < len1 && j < len2; i++) {
|
||||||
|
while (lyric[i][0] > tlyric[j][0] && j + 1 < len2) {
|
||||||
|
j++;
|
||||||
|
}
|
||||||
|
if (lyric[i][0] === tlyric[j][0]) {
|
||||||
|
tlyric[j][2] = tlyric[j][2].replace('/', '');
|
||||||
|
if (tlyric[j][2]) {
|
||||||
|
lyric[i][2] += ' (' + tlyric[j][2] + ')';
|
||||||
|
}
|
||||||
|
j++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (var k = 0; k < len1; k++) {
|
||||||
|
var t = lyric[k][0];
|
||||||
|
result += '['.concat(
|
||||||
|
String(Math.floor(t / 6e4)).padStart(2, '0'),
|
||||||
|
':',
|
||||||
|
String(Math.floor((t % 6e4) / 1e3)).padStart(2, '0'),
|
||||||
|
'.',
|
||||||
|
String(t % 1e3).padStart(3, '0'),
|
||||||
|
']',
|
||||||
|
lyric[k][2],
|
||||||
|
'\n',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 一键载入 ///
|
||||||
|
window.loadAll = function () {
|
||||||
|
userGet.click();
|
||||||
|
vipGet.click();
|
||||||
|
taskGetBtn.click();
|
||||||
|
showResult('正在载入所有数据...', 'info');
|
||||||
|
};
|
||||||
|
|
||||||
|
window.loadLocalCookie();
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,670 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta name="referrer" content="no-referrer" />
|
||||||
|
<link rel="icon" href="docs/netease.png" />
|
||||||
|
<title>云小编每日抽奖 - 网易云音乐 API Enhanced</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f5f5f5;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 520px;
|
||||||
|
margin: 40px auto;
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 32px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle b {
|
||||||
|
color: #d63031;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Section ---- */
|
||||||
|
.section {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section:last-of-type {
|
||||||
|
border-bottom: none;
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Form Elements ---- */
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #555;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
label .tag {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 400;
|
||||||
|
padding: 1px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
margin-left: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
label .tag.o {
|
||||||
|
background: #fef3c7;
|
||||||
|
color: #92400e;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"],
|
||||||
|
input[type="number"],
|
||||||
|
textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 14px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
outline: none;
|
||||||
|
font-family: inherit;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"]:focus,
|
||||||
|
input[type="number"]:focus,
|
||||||
|
textarea:focus {
|
||||||
|
border-color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
background: #333;
|
||||||
|
color: white;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
border: none;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
background: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:disabled {
|
||||||
|
background: #ccc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm {
|
||||||
|
display: inline-block;
|
||||||
|
width: auto;
|
||||||
|
padding: 6px 16px;
|
||||||
|
background: #333;
|
||||||
|
color: white;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
border: none;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm:hover {
|
||||||
|
background: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm:disabled {
|
||||||
|
background: #ccc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm.gray {
|
||||||
|
background: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm.gray:hover {
|
||||||
|
background: #4b5563;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm.gray:disabled {
|
||||||
|
background: #d1d5db;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm.green {
|
||||||
|
background: #059669;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm.green:hover {
|
||||||
|
background: #047857;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm.green:disabled {
|
||||||
|
background: #a7f3d0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-fill {
|
||||||
|
margin-top: 5px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-fill span {
|
||||||
|
color: #0066cc;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-fill span:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Lottery Display ---- */
|
||||||
|
.lottery-display {
|
||||||
|
text-align: center;
|
||||||
|
padding: 24px 16px;
|
||||||
|
background: #f9fafb;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rolling-display {
|
||||||
|
font-size: 2.4rem;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #333;
|
||||||
|
min-height: 4rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rolling-display.idle {
|
||||||
|
color: #999;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Winner Result ---- */
|
||||||
|
.winner-display {
|
||||||
|
background: #f0fdf4;
|
||||||
|
border: 1px solid #86efac;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px 16px;
|
||||||
|
margin-top: 16px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.winner-label {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #065f46;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.winner-name {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #dc2626;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confetti-row {
|
||||||
|
margin-top: 8px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confetti-emoji {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
animation: bounce 0.6s ease infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confetti-emoji:nth-child(2) {
|
||||||
|
animation-delay: 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confetti-emoji:nth-child(3) {
|
||||||
|
animation-delay: 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes bounce {
|
||||||
|
from {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: translateY(-8px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Status ---- */
|
||||||
|
.status-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
background: #f9fafb;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-top: 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #059669;
|
||||||
|
flex-shrink: 0;
|
||||||
|
animation: pulse 1.5s ease infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot.error {
|
||||||
|
background: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot.idle {
|
||||||
|
background: #9ca3af;
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-text {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Footer ---- */
|
||||||
|
.footer-link {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 24px;
|
||||||
|
padding-top: 16px;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-link a {
|
||||||
|
color: #666;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-link a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Responsive ---- */
|
||||||
|
@media (max-width: 500px) {
|
||||||
|
.container {
|
||||||
|
padding: 20px 16px;
|
||||||
|
margin: 20px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rolling-display {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
min-height: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.winner-name {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>云小编每日抽奖</h1>
|
||||||
|
<p class="subtitle">每次抽奖消耗 <b>200 个曲库编辑部积分</b>,每日最多可抽奖 3 次</p>
|
||||||
|
|
||||||
|
<!-- Cookie 管理 -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="cookieInput">令牌 <span class="tag o">可选</span></label>
|
||||||
|
<input type="text" id="cookieInput" placeholder="留空则默认读取本地存储的令牌" autocomplete="off" />
|
||||||
|
<div class="quick-fill">
|
||||||
|
<span onclick="readLocalCookie()">读取本地令牌</span>
|
||||||
|
<span onclick="clearCookie()">清除令牌</span>
|
||||||
|
<span onclick="saveCookie()">保存令牌</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 抽奖区域 -->
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-header">
|
||||||
|
<span class="section-title">每日抽奖</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="lottery-display">
|
||||||
|
<div class="rolling-display idle" id="rollingDisplay">等待抽奖</div>
|
||||||
|
<div id="resultContainer"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn" id="lotteryBtn">
|
||||||
|
🎲 开始抽奖
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="status-bar" id="statusBar">
|
||||||
|
<span class="status-dot idle" id="statusDot"></span>
|
||||||
|
<span class="status-text" id="statusText">就绪,点击按钮开始抽奖</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 页脚 -->
|
||||||
|
<div class="footer-link">
|
||||||
|
<a href="/">← 返回首页</a>
|
||||||
|
<span style="margin: 0 8px; color: #ddd;">·</span>
|
||||||
|
<a href="/ugc.html">云小编任务中心</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
// ============ DOM 元素 ============
|
||||||
|
const cookieInput = document.getElementById('cookieInput');
|
||||||
|
const rollingDisplay = document.getElementById('rollingDisplay');
|
||||||
|
const resultContainer = document.getElementById('resultContainer');
|
||||||
|
const lotteryBtn = document.getElementById('lotteryBtn');
|
||||||
|
const statusDot = document.getElementById('statusDot');
|
||||||
|
const statusText = document.getElementById('statusText');
|
||||||
|
|
||||||
|
// ============ 状态 ============
|
||||||
|
let isDrawing = false;
|
||||||
|
let rollingTimer = null;
|
||||||
|
|
||||||
|
// ============ 状态栏更新 ============
|
||||||
|
function setStatus(type, message) {
|
||||||
|
statusText.textContent = message;
|
||||||
|
statusDot.className = 'status-dot';
|
||||||
|
if (type === 'error') {
|
||||||
|
statusDot.classList.add('error');
|
||||||
|
} else if (type === 'info') {
|
||||||
|
statusDot.style.background = '#059669';
|
||||||
|
} else {
|
||||||
|
statusDot.classList.add('idle');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ 重置结果 ============
|
||||||
|
function resetResult() {
|
||||||
|
resultContainer.innerHTML = '';
|
||||||
|
rollingDisplay.textContent = '等待抽奖';
|
||||||
|
rollingDisplay.className = 'rolling-display idle';
|
||||||
|
setStatus('idle', '就绪,点击按钮开始抽奖');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ 本地令牌管理 ============
|
||||||
|
const cookieKey = 'ncm_cookie';
|
||||||
|
let cookie = localStorage.getItem(cookieKey) || '';
|
||||||
|
if (cookie) {
|
||||||
|
cookieInput.value = cookie;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.readLocalCookie = function () {
|
||||||
|
const local = localStorage.getItem(cookieKey);
|
||||||
|
if (local) {
|
||||||
|
cookie = local;
|
||||||
|
cookieInput.value = local;
|
||||||
|
setStatus('info', '已读取本地令牌');
|
||||||
|
} else {
|
||||||
|
setStatus('error', '未在本地发现令牌,请手动输入');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.clearCookie = function () {
|
||||||
|
cookie = '';
|
||||||
|
cookieInput.value = '';
|
||||||
|
localStorage.removeItem(cookieKey);
|
||||||
|
setStatus('info', '已清除本地令牌');
|
||||||
|
};
|
||||||
|
|
||||||
|
window.saveCookie = function () {
|
||||||
|
const ck = cookieInput.value.trim();
|
||||||
|
if (ck) {
|
||||||
|
cookie = ck;
|
||||||
|
localStorage.setItem(cookieKey, ck);
|
||||||
|
setStatus('info', '已保存本地令牌');
|
||||||
|
} else {
|
||||||
|
setStatus('error', '令牌内容为空,未保存');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function getCookie() {
|
||||||
|
const ck = cookieInput.value.trim();
|
||||||
|
if (ck) cookie = ck;
|
||||||
|
return ck || cookie || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ 调用后端接口 ============
|
||||||
|
async function request(url, params) {
|
||||||
|
try {
|
||||||
|
url += '?timestamp=' + Date.now() + params;
|
||||||
|
const ck = getCookie();
|
||||||
|
if (ck) url += '&cookie=' + encodeURIComponent(ck);
|
||||||
|
|
||||||
|
const response = await fetch(url);
|
||||||
|
|
||||||
|
let res;
|
||||||
|
try {
|
||||||
|
res = await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('数据解析错误:', error);
|
||||||
|
throw new Error('接口请求错误: ' + response.status + ': ' + response.statusText);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.code !== 200) {
|
||||||
|
throw new Error('接口返回错误: ' + res.code + ': ' + (res.message || '未知错误'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('接口请求失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchLotteryResult() {
|
||||||
|
const user = await request('rep/ugc/user/get', '');
|
||||||
|
|
||||||
|
if ((user.data?.availablePoints ?? 0) < 200) {
|
||||||
|
throw new Error('用户剩余积分不足,请先完成任务获取积分');
|
||||||
|
}
|
||||||
|
|
||||||
|
const chance = await request('middle/play/lottery/remain/chance', '&activityId=6501202');
|
||||||
|
|
||||||
|
if (chance.data === 0) {
|
||||||
|
throw new Error('今日机会已用完,明天再来吧');
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await request('middle/play/do/lottery', '&activityId=6501202&drawCount=1');
|
||||||
|
|
||||||
|
const winner = res.data?.prizeDetailInfoMap?.['8186002']?.prizeName;
|
||||||
|
if (!winner) {
|
||||||
|
throw new Error('无法解析响应数据');
|
||||||
|
}
|
||||||
|
|
||||||
|
return [winner, res.data.restChance];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ 滚动动画 ============
|
||||||
|
function startRollingAnimation(finalWinner) {
|
||||||
|
const placeholderNames = ['🎰', '🎲', '🎯', '✨', '💫', '🌟', '🎪', '🎭'];
|
||||||
|
const totalDuration = 2800;
|
||||||
|
const startInterval = 50;
|
||||||
|
const endInterval = 280;
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
rollingDisplay.className = 'rolling-display';
|
||||||
|
resultContainer.innerHTML = '';
|
||||||
|
|
||||||
|
function roll() {
|
||||||
|
const elapsed = Date.now() - startTime;
|
||||||
|
const progress = Math.min(elapsed / totalDuration, 1.0);
|
||||||
|
const easeOutProgress = 1 - Math.pow(1 - progress, 3);
|
||||||
|
const currentInterval = startInterval + (endInterval - startInterval) * easeOutProgress;
|
||||||
|
|
||||||
|
const randomPlaceholder = placeholderNames[Math.floor(Math.random() * placeholderNames.length)];
|
||||||
|
rollingDisplay.textContent = randomPlaceholder;
|
||||||
|
|
||||||
|
if (progress >= 1.0) {
|
||||||
|
clearTimeout(rollingTimer);
|
||||||
|
rollingDisplay.textContent = finalWinner;
|
||||||
|
showWinnerResult(finalWinner);
|
||||||
|
finishDrawing();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
rollingTimer = setTimeout(roll, currentInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
roll();
|
||||||
|
}
|
||||||
|
|
||||||
|
function showWinnerResult(winner) {
|
||||||
|
resultContainer.innerHTML =
|
||||||
|
'<div class="winner-display">' +
|
||||||
|
'<div class="winner-label">🏆 恭喜中奖</div>' +
|
||||||
|
'<div class="winner-name">' + escapeHTML(winner) + '</div>' +
|
||||||
|
'<div class="confetti-row">' +
|
||||||
|
'<span class="confetti-emoji">🎉</span>' +
|
||||||
|
'<span class="confetti-emoji">🎊</span>' +
|
||||||
|
'<span class="confetti-emoji">✨</span>' +
|
||||||
|
'</div>' +
|
||||||
|
'</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHTML(str) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.appendChild(document.createTextNode(str));
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
function finishDrawing() {
|
||||||
|
isDrawing = false;
|
||||||
|
lotteryBtn.disabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ 主抽奖流程 ============
|
||||||
|
async function startLottery() {
|
||||||
|
if (isDrawing) return;
|
||||||
|
|
||||||
|
isDrawing = true;
|
||||||
|
lotteryBtn.disabled = true;
|
||||||
|
resultContainer.innerHTML = '';
|
||||||
|
rollingDisplay.className = 'rolling-display';
|
||||||
|
rollingDisplay.textContent = '🎰';
|
||||||
|
setStatus('info', '正在获取抽奖结果...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetchLotteryResult();
|
||||||
|
setStatus('info', '抽奖成功,剩余次数 ' + res[1]);
|
||||||
|
startRollingAnimation(res[0]);
|
||||||
|
} catch (error) {
|
||||||
|
rollingDisplay.textContent = '抽奖失败';
|
||||||
|
rollingDisplay.className = 'rolling-display idle';
|
||||||
|
setStatus('error', '抽奖失败: ' + error.message);
|
||||||
|
isDrawing = false;
|
||||||
|
lotteryBtn.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ 事件绑定 ============
|
||||||
|
lotteryBtn.addEventListener('click', startLottery);
|
||||||
|
|
||||||
|
document.addEventListener('keydown', function (e) {
|
||||||
|
if (e.key === ' ' && document.activeElement !== cookieInput) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!isDrawing && !lotteryBtn.disabled) {
|
||||||
|
startLottery();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 回车键在输入框中触发保存
|
||||||
|
cookieInput.addEventListener('keypress', function (e) {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
saveCookie();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============ 初始化 ============
|
||||||
|
resetResult();
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
+28
-13
@@ -217,8 +217,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>选择文件</label>
|
<label for="songFile">选择声音文件</label>
|
||||||
<input type="file" name="songFile" accept="audio/*" />
|
<input id="songFile" type="file" name="songFile" accept="audio/*" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="imgFile">选择声音封面(可选)</label>
|
||||||
|
<input id="imgFile" type="file" name="imgFile" accept="image/*" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="btn" @click="submit">上传</button>
|
<button class="btn" @click="submit">上传</button>
|
||||||
@@ -251,29 +256,36 @@
|
|||||||
methods: {
|
methods: {
|
||||||
submit() {
|
submit() {
|
||||||
console.info('submit')
|
console.info('submit')
|
||||||
const file = document.querySelector('input[type=file]').files[0]
|
const songFile = document.querySelector('input[name=songFile]').files[0]
|
||||||
if (!file) {
|
const imgFile = document.querySelector('input[name=imgFile]').files[0]
|
||||||
alert('请选择文件')
|
if (!songFile) {
|
||||||
|
alert('请选择声音文件')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.upload(file)
|
this.upload(songFile, imgFile)
|
||||||
},
|
},
|
||||||
|
|
||||||
async getData() {
|
async getData() {
|
||||||
this.loading = true
|
this.loading = true
|
||||||
try {
|
try {
|
||||||
const res = await axios({
|
const res = await axios({
|
||||||
url: `/voicelist/search?cookie=${localStorage.getItem('cookie')}`,
|
url: `/voicelist/my/created?limit=100&cookie=${localStorage.getItem(
|
||||||
|
'cookie',
|
||||||
|
)}`,
|
||||||
})
|
})
|
||||||
|
|
||||||
console.info(res.data.data)
|
console.info(res.data.data)
|
||||||
this.voicelist = res.data.data.list || []
|
this.voicelist = res.data.data.data || []
|
||||||
this.voicelist.forEach(async (i) => {
|
this.voicelist.forEach(async (i) => {
|
||||||
try {
|
try {
|
||||||
const res2 = await axios({
|
const res2 = await axios({
|
||||||
url: `/voicelist/list?voiceListId=${i.voiceListId}&limit=5`,
|
url: `/voicelist/list?voiceListId=${
|
||||||
|
i.voiceListId
|
||||||
|
}&limit=5&cookie=${localStorage.getItem('cookie')}`,
|
||||||
})
|
})
|
||||||
i.voiceListData = res2.data.data.list || []
|
i.voiceListData = res2.data.data
|
||||||
|
? res2.data.data.list || []
|
||||||
|
: []
|
||||||
console.info(res2)
|
console.info(res2)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('获取播客详情失败:', err)
|
console.error('获取播客详情失败:', err)
|
||||||
@@ -286,14 +298,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
upload(file) {
|
upload(songFile, imgFile) {
|
||||||
if (!this.currentVoice) {
|
if (!this.currentVoice) {
|
||||||
alert('请先选择播客列表')
|
alert('请先选择播客列表')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var formData = new FormData()
|
var formData = new FormData()
|
||||||
formData.append('songFile', file)
|
formData.append('songFile', songFile)
|
||||||
|
if (imgFile) {
|
||||||
|
formData.append('imgFile', imgFile)
|
||||||
|
}
|
||||||
|
|
||||||
axios({
|
axios({
|
||||||
method: 'post',
|
method: 'post',
|
||||||
@@ -312,7 +327,7 @@
|
|||||||
data: formData,
|
data: formData,
|
||||||
})
|
})
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
alert(`${file.name} 上传成功`)
|
alert(`${songFile.name} 上传成功`)
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error('上传失败:', err)
|
console.error('上传失败:', err)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user