mirror of
https://github.com/NeteaseCloudMusicApiEnhanced/api-enhanced.git
synced 2026-10-20 22:51:23 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
19c7dad6db | ||
|
|
2db6684453 | ||
|
|
90de64140f | ||
|
|
2aab9957df | ||
|
|
373d5eedb3 | ||
|
|
c1cbdcb962 | ||
|
|
37b64ab01e | ||
|
|
95367213f7 | ||
|
|
bd407cee81 | ||
|
|
a91ea8f832 | ||
|
|
4a45a14aa7 | ||
|
|
acbea014be | ||
|
|
8ad30b591d | ||
|
|
cb449463fb | ||
|
|
91db12d0f5 | ||
|
|
135df9edda | ||
|
|
548f6c170a | ||
|
|
17c07960f9 | ||
|
|
c4d448641a | ||
|
|
77a9269828 | ||
|
|
9a16c89714 | ||
|
|
a8c781fd64 | ||
|
|
690b462577 | ||
|
|
25ac0c041f | ||
|
|
edef19c3fb | ||
|
|
d55d92cd00 | ||
|
|
d9253bf930 | ||
|
|
f5ce55bcb4 | ||
|
|
4480bd4359 | ||
|
|
e43b123c8a | ||
|
|
ed9bdd6ffa | ||
|
|
7b733e54ab | ||
|
|
edfaf6c0d1 | ||
|
|
f83ad951f6 | ||
|
|
22003c2c32 | ||
|
|
8376699f5e | ||
|
|
5c98a9a1ea | ||
|
|
7c17de7f09 | ||
|
|
a7e8d485f3 | ||
|
|
0957be6e76 | ||
|
|
4a91f77e62 | ||
|
|
a7437f7ca4 | ||
|
|
498945c401 | ||
|
|
7b17a08628 | ||
|
|
0b5c3927c9 | ||
|
|
881267c56d | ||
|
|
d7c0db9455 | ||
|
|
c1d14a86d9 | ||
|
|
a2a2a40d71 | ||
|
|
543660fcdf | ||
|
|
98a95002b2 | ||
|
|
c4a3d83abe | ||
|
|
7753b5de41 | ||
|
|
c1fe1eb925 | ||
|
|
3357317a1d | ||
|
|
a08f425ae7 | ||
|
|
5b780addba | ||
|
|
fc9f41839c | ||
|
|
5859944563 | ||
|
|
aaa6459e55 | ||
|
|
a898e1d86c | ||
|
|
4045f1ad3f | ||
|
|
e6214a3f73 | ||
|
|
686e8bf969 | ||
|
|
9214cb945f | ||
|
|
a79a2d9ef6 | ||
|
|
9608da9b96 | ||
|
|
93b3e624d8 | ||
|
|
15f515c1ba | ||
|
|
e255e8f334 | ||
|
|
16aa61f98e | ||
|
|
a939003b92 | ||
|
|
8f4873f2e2 | ||
|
|
a8b45a8b8b | ||
|
|
1a70281c76 | ||
|
|
f5f4ff6eda | ||
|
|
eb1b5ba0ea | ||
|
|
6732fc7c32 | ||
|
|
6ce6b84015 | ||
|
|
63d89aa906 | ||
|
|
8c212be86b | ||
|
|
89de56d2a2 | ||
|
|
d298025d7c | ||
|
|
a3287a0180 | ||
|
|
9adfd50c58 | ||
|
|
fda7c9e5b3 | ||
|
|
a8266c6d7b | ||
|
|
64a6b9677a | ||
|
|
20d3a1a7a5 | ||
|
|
4826c65042 | ||
|
|
7822ab08d3 | ||
|
|
41bd6d82ce | ||
|
|
6f8dd1d971 | ||
|
|
ddd0af5474 | ||
|
|
fbd2e06419 | ||
|
|
4442fb4341 | ||
|
|
35d1c61cb4 | ||
|
|
a183e153ef | ||
|
|
2fa78c3826 | ||
|
|
5ae1eab14c | ||
|
|
321c25bd7d | ||
|
|
f527c1ef73 | ||
|
|
0a5a1242f2 | ||
|
|
6946dc8e14 | ||
|
|
22b78779ca | ||
|
|
35607f5050 | ||
|
|
60af95ac06 | ||
|
|
46073e5f50 | ||
|
|
bf12abe2f1 | ||
|
|
1491b91829 | ||
|
|
63f669d7f8 | ||
|
|
d525204dda | ||
|
|
6eb31984fd | ||
|
|
9faeb0aa25 | ||
|
|
f6db3e76e9 | ||
|
|
9163d48f0a | ||
|
|
1454ee216c | ||
|
|
83ee2cda34 | ||
|
|
3c352b5752 | ||
|
|
60ee927e32 | ||
|
|
eb07a525eb | ||
|
|
6fc4231142 | ||
|
|
fa69736300 | ||
|
|
73a5e9d788 | ||
|
|
dc6f56e8a8 | ||
|
|
2c9da8507d | ||
|
|
1db354464f | ||
|
|
395a80e74b | ||
|
|
6550c7c827 | ||
|
|
c959866436 | ||
|
|
e3a1c041b6 | ||
|
|
e2324e26da | ||
|
|
f01e770c35 | ||
|
|
abb56ec014 | ||
|
|
0300c1b51e | ||
|
|
9896242333 | ||
|
|
e3812be97b | ||
|
|
e7ff0da4ba | ||
|
|
65261fa658 | ||
|
|
cb5b2e7d0f | ||
|
|
f58fd4b50d | ||
|
|
1a5f91a192 | ||
|
|
50cc26f297 | ||
|
|
c92613b19e | ||
|
|
7aacc8990b | ||
|
|
8e934789a1 | ||
|
|
d26e6b0615 | ||
|
|
a9c33de182 | ||
|
|
d0935f6e42 | ||
|
|
3e0337ab94 | ||
|
|
5cf43c48ef | ||
|
|
68567f4a14 | ||
|
|
d0d71bb4f9 | ||
|
|
ec7eff1697 | ||
|
|
1882c3c31e | ||
|
|
f21f5677e8 | ||
|
|
fc5a76d5e2 | ||
|
|
fbc34a1846 | ||
|
|
3ef5ea341b | ||
|
|
4195c41171 | ||
|
|
232cc3d916 | ||
|
|
1b4af30a49 | ||
|
|
39e0e0718d | ||
|
|
01d391ac91 | ||
|
|
85d70ba9dd | ||
|
|
3cfecbcc1f | ||
|
|
c6e9746ed6 | ||
|
|
d6961ecb4d | ||
|
|
02242eecf6 | ||
|
|
269456def3 | ||
|
|
086e379f2c | ||
|
|
38803a18ae | ||
|
|
3980d09bcd | ||
|
|
64d4847350 | ||
|
|
cf74e90c1d | ||
|
|
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
|
||||||
@@ -8,6 +8,9 @@ ENABLE_PROXY = false
|
|||||||
## 代理配置
|
## 代理配置
|
||||||
PROXY_URL = "https://your-proxy-url.com/?proxy="
|
PROXY_URL = "https://your-proxy-url.com/?proxy="
|
||||||
|
|
||||||
|
### 随机IP设置
|
||||||
|
## 启用随机中国IP(默认关闭);启用后,所有请求默认使用随机中国IP,除非请求参数randomCNIP显式关闭
|
||||||
|
ENABLE_RANDOM_CN_IP = false
|
||||||
|
|
||||||
### UnblockNeteaseMusic 设置项
|
### UnblockNeteaseMusic 设置项
|
||||||
## 启用全局解灰, 无论是否调用参数都会使用解灰(不推荐开启)
|
## 启用全局解灰, 无论是否调用参数都会使用解灰(不推荐开启)
|
||||||
|
|||||||
@@ -14,17 +14,17 @@ body:
|
|||||||
id: terms
|
id: terms
|
||||||
attributes:
|
attributes:
|
||||||
label: 确认事项
|
label: 确认事项
|
||||||
description: 在提交Bug报告前,请确认以下事项
|
description: |
|
||||||
|
在提交Bug报告前,请确认以下事项:
|
||||||
|
|
||||||
|
- 我已经搜索了现有的issues,确认这不是重复问题
|
||||||
|
- 我使用的是最新版本的API, 而且是官方发布的版本,而不是fork或修改版
|
||||||
|
- 不处理别人搭建的线上服务的问题,此项目提供任何线上服务不保证质量
|
||||||
|
- 如果不是提建议,提 issues 如果不照着模版来将不会优先处理或放着不管
|
||||||
|
- 维护项目都是业余时间,精力有限,我只能挑容易解决的issues处理,为了节约双方时间,请尽可能提供足够的有用的信息,给的信息不够我只能根据精力和时间看情况处理,如果模板信息看都不看就删掉,我不会进行任何回复,并且一个月后close掉issue
|
||||||
|
|
||||||
options:
|
options:
|
||||||
- label: 我已经搜索了现有的issues,确认这不是重复问题
|
- label: 我已确认以上事项
|
||||||
required: true
|
|
||||||
- label: 我使用的是最新版本的API, 而且是官方发布的版本,而不是fork或修改版
|
|
||||||
required: true
|
|
||||||
- label: 不处理别人搭建的线上服务的问题,此项目提供任何线上服务不保证质量
|
|
||||||
required: true
|
|
||||||
- label: 如果不是提建议,提 issues 如果不照着模版来将不会优先处理或放着不管
|
|
||||||
required: true
|
|
||||||
- label: 维护项目都是业余时间,精力有限,我只能挑容易解决的issues处理,为了节约双方时间,请尽可能提供足够的有用的信息,给的信息不够我只能根据精力和时间看情况处理,如果模板信息看都不看就删掉,我不会进行任何回复,并且一个月后close掉issue
|
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
- type: input
|
- type: input
|
||||||
@@ -36,29 +36,14 @@ body:
|
|||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
- type: dropdown
|
|
||||||
id: os
|
|
||||||
attributes:
|
|
||||||
label: 操作系统或平台
|
|
||||||
description: 您在哪个操作系统上遇到了这个问题?
|
|
||||||
options:
|
|
||||||
- Windows 10
|
|
||||||
- Windows 11
|
|
||||||
- Ubuntu 20.04
|
|
||||||
- Ubuntu 22.04
|
|
||||||
- macOS
|
|
||||||
- 其他 Linux 发行版
|
|
||||||
- 使用部署平台或其他 (请在描述中说明)
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
id: deployment
|
id: deployment
|
||||||
attributes:
|
attributes:
|
||||||
label: 部署平台
|
label: 部署方式
|
||||||
description:
|
description: 此项目支持本地部署和云平台部署,您使用的是哪种方式?
|
||||||
options:
|
options:
|
||||||
- 我使用的自己的服务器部署
|
- 本机直接运行 (node / pm2)
|
||||||
|
- 本机 Docker 部署
|
||||||
- Vercel
|
- Vercel
|
||||||
- Heroku
|
- Heroku
|
||||||
- Railway
|
- Railway
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
blank_issues_enabled: true
|
blank_issues_enabled: true
|
||||||
contact_links:
|
contact_links:
|
||||||
|
- name: ↑请尽量使用议题模板创建议题↑
|
||||||
|
url: https://github.com/NeteaseCloudMusicApiEnhanced/api-enhanced/issues/new/choose
|
||||||
|
about: 选择适合的议题模板可以帮助我们更快地定位和解决问题
|
||||||
- name: 提问的艺术
|
- name: 提问的艺术
|
||||||
url: https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/main/README-zh_CN.md
|
url: https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/main/README-zh_CN.md
|
||||||
about: 默认所有 Issues 发起者均已了解此处的内容
|
about: 默认所有 Issues 发起者均已了解此处的内容
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
name: Publish Docker image
|
|
||||||
|
|
||||||
on:
|
|
||||||
release:
|
|
||||||
types: [published]
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
packages: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-and-push:
|
|
||||||
if: startsWith(github.event.release.tag_name, 'v')
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v6
|
|
||||||
|
|
||||||
# 读取 package.json 的版本号
|
|
||||||
- name: Read package version
|
|
||||||
id: pkg
|
|
||||||
run: echo "VERSION=$(jq -r .version package.json)" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v3
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
- name: Log in to Docker Hub
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Log in to GitHub Container Registry
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ github.actor }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Build and push Docker image
|
|
||||||
uses: docker/build-push-action@v7
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
push: true
|
|
||||||
tags: |
|
|
||||||
moefurina/ncm-api:latest
|
|
||||||
moefurina/ncm-api:${{ env.VERSION }}
|
|
||||||
ghcr.io/neteasecloudmusicapienhanced/ncm-api:latest
|
|
||||||
ghcr.io/neteasecloudmusicapienhanced/ncm-api:${{ env.VERSION }}
|
|
||||||
platforms: linux/amd64,linux/arm64/v8
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
name: Build and Create PR
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch: # 手动触发
|
|
||||||
push:
|
|
||||||
branches: [main]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
|
||||||
include:
|
|
||||||
- os: ubuntu-latest
|
|
||||||
platform: linux
|
|
||||||
target: node18-linux-x64
|
|
||||||
output: precompiled/app
|
|
||||||
- os: windows-latest
|
|
||||||
platform: win
|
|
||||||
target: node18-win-x64
|
|
||||||
output: precompiled/app.exe
|
|
||||||
- os: macos-latest
|
|
||||||
platform: macos
|
|
||||||
target: node18-macos-x64
|
|
||||||
output: precompiled/app
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v6
|
|
||||||
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v6
|
|
||||||
with:
|
|
||||||
node-version: '18'
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
npm install -g pnpm
|
|
||||||
pnpm install
|
|
||||||
|
|
||||||
- name: Build for ${{ matrix.platform }}
|
|
||||||
run: |
|
|
||||||
npm run pkg${{ matrix.platform }}
|
|
||||||
env:
|
|
||||||
PKG_TARGET: ${{ matrix.target }}
|
|
||||||
|
|
||||||
- name: Upload artifacts
|
|
||||||
uses: actions/upload-artifact@v7
|
|
||||||
with:
|
|
||||||
name: app-${{ matrix.platform }}
|
|
||||||
path: ${{ matrix.output }}
|
|
||||||
if-no-files-found: error
|
|
||||||
|
|
||||||
create-pr:
|
|
||||||
needs: build
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: github.event_name == 'workflow_dispatch' # 只在手动触发时创建PR
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v6
|
|
||||||
|
|
||||||
- name: Download all artifacts
|
|
||||||
uses: actions/download-artifact@v8
|
|
||||||
with:
|
|
||||||
path: precompiled
|
|
||||||
|
|
||||||
- name: Display structure of downloaded files
|
|
||||||
run: ls -R
|
|
||||||
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v6
|
|
||||||
|
|
||||||
- name: Create new branch and commit
|
|
||||||
run: |
|
|
||||||
git config --global user.name 'github-actions[bot]'
|
|
||||||
git config --global user.email 'github-actions[bot]@users.noreply.github.com'
|
|
||||||
|
|
||||||
BRANCH_NAME="auto-build-$(date +%Y%m%d-%H%M%S)"
|
|
||||||
echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_ENV
|
|
||||||
git checkout -b $BRANCH_NAME
|
|
||||||
|
|
||||||
# 复制并整理下载的文件
|
|
||||||
mkdir -p precompiled
|
|
||||||
cp app-win/* precompiled/ || true
|
|
||||||
cp app-linux/* precompiled/ || true
|
|
||||||
cp app-macos/* precompiled/ || true
|
|
||||||
|
|
||||||
# 提交更改
|
|
||||||
git add precompiled/
|
|
||||||
git commit -m "Auto-build: Add compiled binaries for win, linux, macos" || exit 0
|
|
||||||
|
|
||||||
# 推送到远程仓库
|
|
||||||
git push origin $BRANCH_NAME
|
|
||||||
|
|
||||||
- name: Create Pull Request
|
|
||||||
uses: actions/github-script@v8
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
const { data: pullRequest } = await github.rest.pulls.create({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
title: 'Auto-build: Add compiled binaries for win, linux, macos',
|
|
||||||
head: '${{ env.BRANCH_NAME }}',
|
|
||||||
base: 'main',
|
|
||||||
body: 'This PR contains newly built binaries for Windows, Linux, and macOS platforms.'
|
|
||||||
});
|
|
||||||
console.log(`Created PR #${pullRequest.number}: ${pullRequest.html_url}`);
|
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
name: Build Artifacts
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch: # 手动触发
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||||
|
include:
|
||||||
|
- os: ubuntu-latest
|
||||||
|
platform: linux
|
||||||
|
target: node18-linux-x64
|
||||||
|
output: precompiled/app
|
||||||
|
- os: windows-latest
|
||||||
|
platform: win
|
||||||
|
target: node18-win-x64
|
||||||
|
output: precompiled/app.exe
|
||||||
|
- os: macos-latest
|
||||||
|
platform: macos
|
||||||
|
target: node18-macos-x64
|
||||||
|
output: precompiled/app
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v6
|
||||||
|
with:
|
||||||
|
node-version: '18'
|
||||||
|
|
||||||
|
- name: Setup pnpm
|
||||||
|
run: npm install -g pnpm@9
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Build for ${{ matrix.platform }}
|
||||||
|
run: |
|
||||||
|
npm run pkg${{ matrix.platform }}
|
||||||
|
env:
|
||||||
|
PKG_TARGET: ${{ matrix.target }}
|
||||||
|
|
||||||
|
- name: Upload artifacts
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
with:
|
||||||
|
name: app-${{ matrix.platform }}
|
||||||
|
path: ${{ matrix.output }}
|
||||||
|
if-no-files-found: error
|
||||||
@@ -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: |
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
name: Node.js CI
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
branches: [main]
|
|
||||||
push:
|
|
||||||
branches: [main]
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test:
|
|
||||||
name: Test
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
node-version: [16.x, 18.x, 22.x]
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v5
|
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
|
||||||
uses: actions/setup-node@v6
|
|
||||||
with:
|
|
||||||
node-version: ${{ matrix.node-version }}
|
|
||||||
# Use built-in package manager cache for pnpm
|
|
||||||
cache: 'pnpm'
|
|
||||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
|
||||||
- name: Install pnpm
|
|
||||||
run: npm install -g pnpm
|
|
||||||
- name: Install dependencies (pnpm)
|
|
||||||
run: pnpm install --frozen-lockfile
|
|
||||||
- name: Test
|
|
||||||
env:
|
|
||||||
NCM_API_TEST_LOGIN_COUNTRY_CODE: ${{ secrets.NCM_API_TEST_LOGIN_COUNTRY_CODE }}
|
|
||||||
NCM_API_TEST_LOGIN_PHONE: ${{ secrets.NCM_API_TEST_LOGIN_PHONE }}
|
|
||||||
NCM_API_TEST_LOGIN_PASSWORD: ${{ secrets.NCM_API_TEST_LOGIN_PASSWORD }}
|
|
||||||
run: pnpm test
|
|
||||||
lint:
|
|
||||||
name: Lint
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
node-version: [18.x]
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v5
|
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
|
||||||
uses: actions/setup-node@v6
|
|
||||||
with:
|
|
||||||
node-version: ${{ matrix.node-version }}
|
|
||||||
cache: 'pnpm'
|
|
||||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
|
||||||
- name: Install pnpm
|
|
||||||
run: npm install -g pnpm
|
|
||||||
- name: Install dependencies (pnpm)
|
|
||||||
run: pnpm install --frozen-lockfile
|
|
||||||
- name: Lint
|
|
||||||
run: pnpm run lint
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
# .github/workflows/release.yml
|
|
||||||
name: Release
|
|
||||||
|
|
||||||
on:
|
|
||||||
release:
|
|
||||||
types: [published]
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
release:
|
|
||||||
if: startsWith(github.event.release.tag_name, 'v')
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
id-token: write
|
|
||||||
contents: read
|
|
||||||
packages: write
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
|
|
||||||
# 发布到 NPM Registry
|
|
||||||
- name: Setup Node.js for NPM
|
|
||||||
uses: actions/setup-node@v6
|
|
||||||
with:
|
|
||||||
node-version: 20
|
|
||||||
registry-url: 'https://registry.npmjs.org/'
|
|
||||||
- run: npm install
|
|
||||||
- name: Publish to NPM
|
|
||||||
run: npm publish --provenance --access public
|
|
||||||
|
|
||||||
# 发布到 GitHub Packages
|
|
||||||
- name: Setup Node.js for GitHub Packages
|
|
||||||
uses: actions/setup-node@v6
|
|
||||||
with:
|
|
||||||
node-version: 20
|
|
||||||
registry-url: 'https://npm.pkg.github.com'
|
|
||||||
- name: Publish to GitHub Packages
|
|
||||||
run: npm publish --access public
|
|
||||||
env:
|
|
||||||
NODE_AUTH_TOKEN: ${{ github.token }}
|
|
||||||
@@ -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 }}
|
||||||
@@ -0,0 +1,315 @@
|
|||||||
|
name: Create Release on Version Change
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- package.json
|
||||||
|
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: release-${{ github.ref }}
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
detect:
|
||||||
|
name: Detect Version Change
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
outputs:
|
||||||
|
version: ${{ steps.version.outputs.value }}
|
||||||
|
should_release: ${{ steps.check.outputs.should_release }}
|
||||||
|
tag_exists: ${{ steps.tagcheck.outputs.exists }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Extract version from package.json
|
||||||
|
id: version
|
||||||
|
run: |
|
||||||
|
echo "value=$(jq -r .version package.json)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Check if version changed
|
||||||
|
id: check
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
|
||||||
|
echo "should_release=true" >> $GITHUB_OUTPUT
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if git diff "${{ github.event.before }}" "${{ github.sha }}" -- package.json | grep -q '"version"'; then
|
||||||
|
echo "should_release=true" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "should_release=false" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Check if tag already exists
|
||||||
|
id: tagcheck
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.version.outputs.value }}"
|
||||||
|
|
||||||
|
if git rev-parse "v$VERSION" >/dev/null 2>&1; then
|
||||||
|
echo "exists=true" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "exists=false" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
build:
|
||||||
|
name: Build ${{ matrix.platform }}
|
||||||
|
needs: detect
|
||||||
|
|
||||||
|
if: |
|
||||||
|
needs.detect.outputs.should_release == 'true' &&
|
||||||
|
needs.detect.outputs.tag_exists != 'true'
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- os: ubuntu-latest
|
||||||
|
platform: linux
|
||||||
|
target: node18-linux-x64
|
||||||
|
output: ncm-api-linux-x64
|
||||||
|
|
||||||
|
- os: windows-latest
|
||||||
|
platform: windows
|
||||||
|
target: node18-win-x64
|
||||||
|
output: ncm-api-win-x64.exe
|
||||||
|
|
||||||
|
- os: macos-latest
|
||||||
|
platform: macos
|
||||||
|
target: node18-macos-x64
|
||||||
|
output: ncm-api-macos-x64
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Setup pnpm
|
||||||
|
uses: pnpm/action-setup@v6
|
||||||
|
with:
|
||||||
|
version: 9
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v6
|
||||||
|
with:
|
||||||
|
node-version: 24
|
||||||
|
cache: pnpm
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Build binary
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
PKG_TARGET: ${{ matrix.target }}
|
||||||
|
run: |
|
||||||
|
mkdir -p release-artifacts
|
||||||
|
|
||||||
|
case "${{ matrix.platform }}" in
|
||||||
|
linux)
|
||||||
|
npm run pkglinux
|
||||||
|
mv precompiled/app "release-artifacts/${{ matrix.output }}"
|
||||||
|
;;
|
||||||
|
|
||||||
|
windows)
|
||||||
|
npm run pkgwin
|
||||||
|
mv precompiled/app.exe "release-artifacts/${{ matrix.output }}"
|
||||||
|
;;
|
||||||
|
|
||||||
|
macos)
|
||||||
|
npm run pkgmacos
|
||||||
|
mv precompiled/app "release-artifacts/${{ matrix.output }}"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
- name: Upload build artifact
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
with:
|
||||||
|
name: ${{ matrix.platform }}-binary
|
||||||
|
path: release-artifacts/*
|
||||||
|
|
||||||
|
release:
|
||||||
|
name: Create GitHub Release
|
||||||
|
|
||||||
|
needs:
|
||||||
|
- detect
|
||||||
|
- build
|
||||||
|
|
||||||
|
if: |
|
||||||
|
needs.detect.outputs.should_release == 'true' &&
|
||||||
|
needs.detect.outputs.tag_exists != 'true'
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
fetch-tags: true
|
||||||
|
|
||||||
|
- name: Download build artifacts
|
||||||
|
uses: actions/download-artifact@v8
|
||||||
|
with:
|
||||||
|
path: release-artifacts
|
||||||
|
merge-multiple: true
|
||||||
|
|
||||||
|
- name: List flattened artifacts
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
mkdir -p final-artifacts
|
||||||
|
cp release-artifacts/* final-artifacts/
|
||||||
|
ls -lah final-artifacts
|
||||||
|
|
||||||
|
- name: Generate release notes
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
VERSION="${{ needs.detect.outputs.version }}"
|
||||||
|
|
||||||
|
PREV_TAG=$(git tag --sort=-v:refname | head -1)
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "# Release v${VERSION}"
|
||||||
|
echo ""
|
||||||
|
echo "## 更新内容 / Changelog"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [ -n "$PREV_TAG" ]; then
|
||||||
|
echo "从 \`$PREV_TAG\` 到 \`v$VERSION\` 的提交记录:"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
git log "$PREV_TAG..HEAD" \
|
||||||
|
--no-merges \
|
||||||
|
--pretty=format:"- %s (%h)"
|
||||||
|
|
||||||
|
else
|
||||||
|
echo "首次发布,包含以下提交:"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
git log \
|
||||||
|
--no-merges \
|
||||||
|
--pretty=format:"- %s (%h)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo ""
|
||||||
|
echo "---"
|
||||||
|
echo "自动发布 via GitHub Actions"
|
||||||
|
|
||||||
|
} > release-notes.md
|
||||||
|
|
||||||
|
cat release-notes.md
|
||||||
|
|
||||||
|
- name: Create Git tag
|
||||||
|
run: |
|
||||||
|
git tag "v${{ needs.detect.outputs.version }}"
|
||||||
|
git push origin "v${{ needs.detect.outputs.version }}"
|
||||||
|
|
||||||
|
- name: Create GitHub Release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
tag_name: v${{ needs.detect.outputs.version }}
|
||||||
|
name: Release v${{ needs.detect.outputs.version }}
|
||||||
|
body_path: release-notes.md
|
||||||
|
files: final-artifacts/*
|
||||||
|
draft: false
|
||||||
|
prerelease: false
|
||||||
|
|
||||||
|
publish-docker:
|
||||||
|
name: Publish Docker Image
|
||||||
|
needs: detect
|
||||||
|
|
||||||
|
if: |
|
||||||
|
needs.detect.outputs.should_release == 'true' &&
|
||||||
|
needs.detect.outputs.tag_exists != 'true'
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Read package version
|
||||||
|
id: pkg
|
||||||
|
run: echo "VERSION=$(jq -r .version package.json)" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Log in to Docker Hub
|
||||||
|
uses: docker/login-action@v4
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Log in to GitHub Container Registry
|
||||||
|
uses: docker/login-action@v4
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and push Docker image
|
||||||
|
uses: docker/build-push-action@v7
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
moefurina/ncm-api:latest
|
||||||
|
moefurina/ncm-api:${{ env.VERSION }}
|
||||||
|
ghcr.io/neteasecloudmusicapienhanced/ncm-api:latest
|
||||||
|
ghcr.io/neteasecloudmusicapienhanced/ncm-api:${{ env.VERSION }}
|
||||||
|
platforms: linux/amd64,linux/arm64/v8
|
||||||
|
|
||||||
|
publish-npm:
|
||||||
|
name: Publish to npm
|
||||||
|
needs: detect
|
||||||
|
|
||||||
|
if: |
|
||||||
|
needs.detect.outputs.should_release == 'true' &&
|
||||||
|
needs.detect.outputs.tag_exists != 'true'
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
id-token: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- uses: pnpm/action-setup@v6
|
||||||
|
with:
|
||||||
|
version: 9
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v6
|
||||||
|
with:
|
||||||
|
node-version: 24
|
||||||
|
cache: pnpm
|
||||||
|
|
||||||
|
- run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- run: pnpm publish --access public --provenance
|
||||||
@@ -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` 白名单;改依赖安装行为时先看它。
|
||||||
-1418
File diff suppressed because it is too large
Load Diff
+8
-3
@@ -2,15 +2,20 @@ 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
|
||||||
|
|
||||||
CMD [ "/sbin/tini", "--", "node", "app.js" ]
|
CMD [ "/sbin/tini", "--", "node", "app.js" ]
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
|
|
||||||
## 项目简介
|
## 项目简介
|
||||||
|
|
||||||
网易云音乐第三方 Node.js API, 支持丰富的音乐相关接口,适合自建服务、二次开发和多平台部署(如果原版诈尸, 我会及时同步 or 归档)。
|
网易云音乐第三方 Node.js API, 支持丰富的音乐相关接口,适合自建服务、二次开发和多平台部署
|
||||||
|
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
>
|
>
|
||||||
@@ -45,14 +45,14 @@
|
|||||||
|
|
||||||
### 环境要求
|
### 环境要求
|
||||||
|
|
||||||
- Node.js 18 及以上
|
- Node.js 22 及以上
|
||||||
- 推荐使用 pnpm 进行依赖管理
|
- 推荐使用 pnpm 进行依赖管理
|
||||||
|
|
||||||
### 安装
|
### 安装
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/neteasecloudmusicapienhanced/api-enhanced.git
|
git clone https://github.com/neteasecloudmusicapienhanced/api-enhanced.git
|
||||||
cd api
|
cd api-enhanced
|
||||||
pnpm i
|
pnpm i
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -131,6 +131,7 @@ $ sudo docker run -d -p 3000:3000 ncm-api
|
|||||||
| **CORS_ALLOW_ORIGIN** | `*` | 允许跨域请求的域名。可填写单个源,或使用逗号分隔多个源(例如 `https://a.com,https://b.com`)。 |
|
| **CORS_ALLOW_ORIGIN** | `*` | 允许跨域请求的域名。可填写单个源,或使用逗号分隔多个源(例如 `https://a.com,https://b.com`)。 |
|
||||||
| **ENABLE_PROXY** | `false` | 是否启用反向代理功能。 |
|
| **ENABLE_PROXY** | `false` | 是否启用反向代理功能。 |
|
||||||
| **PROXY_URL** | `https://your-proxy-url.com/?proxy=` | 代理服务地址。仅当 `ENABLE_PROXY=true` 时生效。 |
|
| **PROXY_URL** | `https://your-proxy-url.com/?proxy=` | 代理服务地址。仅当 `ENABLE_PROXY=true` 时生效。 |
|
||||||
|
| **ENABLE_RANDOM_CN_IP** | `false` | 是否默认启用随机中国IP。启用后,所有请求默认使用随机中国IP,除非请求参数 `randomCNIP` 显式关闭。 |
|
||||||
| **ENABLE_GENERAL_UNBLOCK** | `true` | 是否启用全局解灰(推荐开启)。开启后所有歌曲都尝试自动解锁。 |
|
| **ENABLE_GENERAL_UNBLOCK** | `true` | 是否启用全局解灰(推荐开启)。开启后所有歌曲都尝试自动解锁。 |
|
||||||
| **ENABLE_FLAC** | `true` | 是否启用无损音质(FLAC)。 |
|
| **ENABLE_FLAC** | `true` | 是否启用无损音质(FLAC)。 |
|
||||||
| **SELECT_MAX_BR** | `false` | 启用无损音质时,是否选择最高码率音质。 |
|
| **SELECT_MAX_BR** | `false` | 启用无损音质时,是否选择最高码率音质。 |
|
||||||
@@ -214,7 +215,7 @@ pnpm test
|
|||||||
|
|
||||||
原作者 [Binaryify/NeteaseCloudMusicApi](https://github.com/binaryify/NeteaseCloudMusicApi) 项目为本项目基础 (该项目在`npmjs`网站上仍持续维护, 但 github 仓库已不再更新)
|
原作者 [Binaryify/NeteaseCloudMusicApi](https://github.com/binaryify/NeteaseCloudMusicApi) 项目为本项目基础 (该项目在`npmjs`网站上仍持续维护, 但 github 仓库已不再更新)
|
||||||
|
|
||||||
感谢大佬们为逆向eapi, weapi等加密算法所做的贡献
|
感谢大佬们为逆向eapi, weapi, xeapi等加密算法所做的贡献
|
||||||
|
|
||||||
项目参考:
|
项目参考:
|
||||||
|
|
||||||
@@ -224,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 生态
|
||||||
|
|
||||||
| 语言 | 作者 | 地址 | 类型 |
|
| 语言 | 作者 | 地址 | 类型 |
|
||||||
@@ -243,6 +246,8 @@ pnpm test
|
|||||||
|
|
||||||
- [Yueby/music-together](https://github.com/Yueby/music-together)
|
- [Yueby/music-together](https://github.com/Yueby/music-together)
|
||||||
|
|
||||||
|
- [chthollyphlie/folia-major](https://github.com/chthollyphile/folia-major)
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
[MIT License](https://github.com/MoeFurina/NeteaseCloudMusicApiEnhanced/blob/main/LICENSE)
|
[MIT License](https://github.com/MoeFurina/NeteaseCloudMusicApiEnhanced/blob/main/LICENSE)
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ const fs = require('fs')
|
|||||||
const path = require('path')
|
const path = require('path')
|
||||||
const { register_anonimous } = require('./main')
|
const { register_anonimous } = require('./main')
|
||||||
const { cookieToJson, generateRandomChineseIP } = require('./util/index')
|
const { cookieToJson, generateRandomChineseIP } = require('./util/index')
|
||||||
|
const { getXeapiPublicKey } = require('./util/xeapiKey')
|
||||||
|
const { 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() {
|
||||||
@@ -20,5 +23,33 @@ async function generateConfig() {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
console.log(error)
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
let currentPublicKey = {}
|
||||||
|
try {
|
||||||
|
currentPublicKey = JSON.parse(
|
||||||
|
fs.readFileSync(path.resolve(tmpPath, 'xeapi_public_key'), 'utf-8'),
|
||||||
|
)
|
||||||
|
} catch (_) {}
|
||||||
|
const publicKey = await getXeapiPublicKey(currentPublicKey, global.deviceId)
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.resolve(tmpPath, 'xeapi_public_key'),
|
||||||
|
JSON.stringify(publicKey),
|
||||||
|
'utf-8',
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const current = loadNeapiKey()
|
||||||
|
await registerNeapikey(
|
||||||
|
{
|
||||||
|
version: current ? current.version : 0,
|
||||||
|
behavior: current ? 'update' : 'open',
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
module.exports = generateConfig
|
module.exports = generateConfig
|
||||||
|
|||||||
Vendored
+1074
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,51 @@
|
|||||||
|
// 获取广告
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
|
||||||
|
module.exports = async (query, request) => {
|
||||||
|
const data = {
|
||||||
|
type_ids: query.type_ids || '["400002_0"]',
|
||||||
|
}
|
||||||
|
|
||||||
|
const option = createOption(query, 'xeapi', 'v3')
|
||||||
|
|
||||||
|
const res = await request(`/api/ad/get`, data, option)
|
||||||
|
const raw = res.body
|
||||||
|
|
||||||
|
// 提取广告中的 req_id
|
||||||
|
let reqId = ''
|
||||||
|
try {
|
||||||
|
if (raw?.ads) {
|
||||||
|
const ad = Object.values(raw.ads)[0]
|
||||||
|
// 逆向 v9.5.61:客户端从 ad.adExtMap["req_id"] 取 reqUid
|
||||||
|
if (ad?.adExtMap) {
|
||||||
|
if (typeof ad.adExtMap === 'string') {
|
||||||
|
try {
|
||||||
|
reqId = JSON.parse(ad.adExtMap).req_id || ''
|
||||||
|
} catch (_) {}
|
||||||
|
} else {
|
||||||
|
reqId = ad.adExtMap.req_id || ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 兜底:adLogId.requestId / ad.reqId / extJson.contextInfo.req_id
|
||||||
|
if (!reqId && ad?.adLogId?.requestId) reqId = ad.adLogId.requestId
|
||||||
|
if (!reqId && ad?.reqId) reqId = ad.reqId
|
||||||
|
if (!reqId && ad?.extJson) {
|
||||||
|
try {
|
||||||
|
const ext = JSON.parse(ad.extJson)
|
||||||
|
reqId = ext?.contextInfo?.req_id || ''
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
code: 200,
|
||||||
|
ads: raw?.ads || null,
|
||||||
|
message: raw?.message || null,
|
||||||
|
extra: { reqId },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
// 获取免费听时长状态
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
entrance: 'FREE_LISTEN_RN',
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
`/api/ad/homepage/free/tab/extend/v2`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'xeapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,252 @@
|
|||||||
|
// 看广告免费听歌 - 领取免费听权益
|
||||||
|
// 请求流程(基于逆向网易云音乐 v9.5.61 原生 Kotlin 源码,classes5/18/19.dex):
|
||||||
|
// 1. 从广告平台拉广告 → 用户看完/点击广告 → 获取 ad 对象的 extJson.contextInfo.req_id 作为 reqUid
|
||||||
|
// 2. 调用本接口传入 reqUid 及相关权益参数,领取权益
|
||||||
|
// 3. 服务器返回 gainFlag 等标识,用于展示领取结果
|
||||||
|
//
|
||||||
|
// 调用链(逆向还原):
|
||||||
|
// AdDSLIncentiveVideoRightsHelper.requestRightsGainInner
|
||||||
|
// → AdDSLUtils.requestRightGain(ad, exposeTime, clickTime, creativeType, cb)
|
||||||
|
// → 构造 ListeningRightRequestParams(21 字段)→ JSON.stringify
|
||||||
|
// → body = { "reqParam": "..." } → POST /api/ad/listening/rights/gain
|
||||||
|
// (注意:客户端 AdDSLIncentiveVideoRightsHelper 写死 creativeType=36)
|
||||||
|
//
|
||||||
|
// 接口不只领取听歌时长,还包含"看视频得云贝"等权益:
|
||||||
|
// rightsGainMethod=6 (LAXIN_EXPOSE_OR_DOWNLOAD 拉新曝光/下载分段权益) 时,
|
||||||
|
// 弹窗 AdLaxinSegmentedGuideDialog 展示 "获得X云贝"(2000 云贝等),
|
||||||
|
// 权益数值由广告下发的 AdLaxinSegmentedRightsGuidePopup.rightsValue 决定,
|
||||||
|
// extraRightsType 作为权益类型随本接口上传。
|
||||||
|
//
|
||||||
|
// rightsGainMethod 枚举(h80/a):
|
||||||
|
// 1=EXPOSE 曝光, 2=EXPOSE_CLICK 曝光+点击, 3=EXPOSE_DOWNLOAD 曝光+下载,
|
||||||
|
// 4=CLICK_STAY 点击+停留, 5=EXPOSURE_OR_CLICK_STAY 曝光或点击停留,
|
||||||
|
// 6=LAXIN_EXPOSE_OR_DOWNLOAD 拉新曝光/下载分段权益
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
const adGet = require('./ad_get.js')
|
||||||
|
|
||||||
|
// 安全 JSON 解析(字符串字段可能是 JSON 文本)
|
||||||
|
function safeParse(str, fallback) {
|
||||||
|
if (typeof str !== 'string') return fallback
|
||||||
|
try {
|
||||||
|
return JSON.parse(str)
|
||||||
|
} catch (_) {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = async (query, request) => {
|
||||||
|
const time = Date.now()
|
||||||
|
|
||||||
|
// 从广告对象自动补齐请求字段。
|
||||||
|
// 实测 v9.5.61 真实 API 返回(/ad/get)确认的字段路径:
|
||||||
|
// reqUid = ad.extJson.contextInfo.req_id(真实响应中唯一存在)
|
||||||
|
// contextInfo = ad.extJson.contextInfo(完整对象序列化)
|
||||||
|
// generalRightsInfo = ad.generalRightsInfo(字符串 JSON)
|
||||||
|
// creativeType = ad.creativeType
|
||||||
|
// rightsGainMethod / extraRightsType / rightsGainDuration / rightsGainType /
|
||||||
|
// extraRightsGainMethod / extraRightsGainDuration / nextRightsGainDuration /
|
||||||
|
// rightsExtJson / source / rightsUpperLimit / qualified
|
||||||
|
// = ad.generalRightsInfo(字符串)解析后取值
|
||||||
|
// (逆向类字段 adExtMap/adLogId/listeningRightHintInfo 真实响应中不存在,仅兜底)
|
||||||
|
// sniffTime = method==3 或 6 时 currentTimeMillis
|
||||||
|
let reqUid = query.reqUid || ''
|
||||||
|
let contextInfo = query.contextInfo
|
||||||
|
let creativeType = query.creativeType
|
||||||
|
let generalRightsInfo = query.generalRightsInfo
|
||||||
|
|
||||||
|
// 广告 hint 配置:仅当调用方未显式传参时用广告下发值兜底
|
||||||
|
const hint = {}
|
||||||
|
|
||||||
|
if (!reqUid || contextInfo === undefined || creativeType === undefined) {
|
||||||
|
try {
|
||||||
|
const adRes = await adGet(
|
||||||
|
{ ...query, type_ids: query.type_ids || '["400002_0"]' },
|
||||||
|
request,
|
||||||
|
)
|
||||||
|
const ad = Object.values(adRes?.body?.ads || {})[0]
|
||||||
|
if (!ad) throw new Error('ads 为空(未登录或广告位无广告)')
|
||||||
|
|
||||||
|
// reqUid:真实 API 返回中 req_id 在 ad.extJson.contextInfo.req_id(实测 v9.5.61)
|
||||||
|
// (逆向类字段 adExtMap/adLogId 在真实响应中不存在,仅作兜底)
|
||||||
|
if (!reqUid) {
|
||||||
|
const ext =
|
||||||
|
typeof ad?.extJson === 'string'
|
||||||
|
? safeParse(ad.extJson, {})
|
||||||
|
: ad?.extJson || {}
|
||||||
|
const extMap =
|
||||||
|
typeof ad?.adExtMap === 'string'
|
||||||
|
? safeParse(ad.adExtMap, {})
|
||||||
|
: ad?.adExtMap
|
||||||
|
reqUid =
|
||||||
|
ext?.contextInfo?.req_id ||
|
||||||
|
extMap?.req_id ||
|
||||||
|
ad?.adLogId?.requestId ||
|
||||||
|
ad?.reqId ||
|
||||||
|
adRes?.body?.extra?.reqId ||
|
||||||
|
''
|
||||||
|
}
|
||||||
|
// contextInfo:真实 API 返回中在 ad.extJson.contextInfo(实测 v9.5.61)
|
||||||
|
if (contextInfo === undefined) {
|
||||||
|
const ext =
|
||||||
|
typeof ad?.extJson === 'string'
|
||||||
|
? safeParse(ad.extJson, {})
|
||||||
|
: ad?.extJson || {}
|
||||||
|
const ci =
|
||||||
|
ext?.contextInfo || ad?.adLogId?.contextInfo || ad?.showContext
|
||||||
|
if (ci) {
|
||||||
|
contextInfo = typeof ci === 'string' ? ci : JSON.stringify(ci)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (creativeType === undefined && ad?.creativeType !== undefined) {
|
||||||
|
creativeType = ad.creativeType
|
||||||
|
}
|
||||||
|
if (generalRightsInfo === undefined && ad?.generalRightsInfo) {
|
||||||
|
generalRightsInfo =
|
||||||
|
typeof ad.generalRightsInfo === 'string'
|
||||||
|
? ad.generalRightsInfo
|
||||||
|
: JSON.stringify(ad.generalRightsInfo)
|
||||||
|
}
|
||||||
|
// 缓存广告下发的领取配置(后续用于兜底)
|
||||||
|
// 实测 v9.5.61:rightsGainMethod/rightsUpperLimit/qualified 等在
|
||||||
|
// ad.generalRightsInfo(字符串)里,ad.listeningRightHintInfo 真实响应中不存在
|
||||||
|
const hintCfg =
|
||||||
|
typeof ad?.listeningRightHintInfo === 'string'
|
||||||
|
? safeParse(ad.listeningRightHintInfo, {})
|
||||||
|
: ad?.listeningRightHintInfo || {}
|
||||||
|
const gri =
|
||||||
|
typeof ad?.generalRightsInfo === 'string'
|
||||||
|
? safeParse(ad.generalRightsInfo, {})
|
||||||
|
: ad?.generalRightsInfo || {}
|
||||||
|
Object.assign(hint, hintCfg, gri)
|
||||||
|
console.log(`自动获取 reqUid: ${reqUid}`)
|
||||||
|
} catch (e) {
|
||||||
|
// 获取广告失败,后续请求会因缺少 reqUid 被拒绝
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const rightsGainMethod = query.rightsGainMethod
|
||||||
|
? parseInt(query.rightsGainMethod)
|
||||||
|
: hint.rightsGainMethod || 2
|
||||||
|
|
||||||
|
const rightsParam = {
|
||||||
|
// 必填: 广告请求 ID,自动从 ad_get 获取
|
||||||
|
reqUid,
|
||||||
|
|
||||||
|
// 曝光时间戳
|
||||||
|
exposureTime: query.exposureTime ? parseInt(query.exposureTime) : time,
|
||||||
|
|
||||||
|
// 当前登录用户 ID(原版客户端从 Profile.getUserId() 获取)
|
||||||
|
userId: query.uid ? parseInt(query.uid) : undefined,
|
||||||
|
|
||||||
|
// 点击时间戳
|
||||||
|
clickTime: query.clickTime ? parseInt(query.clickTime) : time,
|
||||||
|
|
||||||
|
// 额外权益类型(拉新分段权益等,决定发放云贝/时长/下载,来自广告配置)
|
||||||
|
extraRightsType: query.extraRightsType
|
||||||
|
? parseInt(query.extraRightsType)
|
||||||
|
: hint.extraRightsType !== undefined
|
||||||
|
? parseInt(hint.extraRightsType)
|
||||||
|
: undefined,
|
||||||
|
|
||||||
|
// 是否连续播放(默认 false)
|
||||||
|
playContinuously: query.playContinuously ? true : false,
|
||||||
|
|
||||||
|
// 来源标识(原版从 ad.listeningRightHintInfo.source 读取)
|
||||||
|
source: query.source
|
||||||
|
? parseInt(query.source)
|
||||||
|
: hint.source !== undefined
|
||||||
|
? parseInt(hint.source)
|
||||||
|
: undefined,
|
||||||
|
|
||||||
|
// 广告创意类型(激励视频场景=36,优先取 query / 广告对象)
|
||||||
|
creativeType: creativeType !== undefined ? parseInt(creativeType) : 36,
|
||||||
|
|
||||||
|
// 权益领取方式(1~6 枚举,见文件头注释)
|
||||||
|
rightsGainMethod,
|
||||||
|
|
||||||
|
// 权益扩展方式与时长
|
||||||
|
extraRightsGainMethod: query.extraRightsGainMethod
|
||||||
|
? parseInt(query.extraRightsGainMethod)
|
||||||
|
: hint.extraRightsGainMethod !== undefined
|
||||||
|
? parseInt(hint.extraRightsGainMethod)
|
||||||
|
: undefined,
|
||||||
|
extraRightsGainDuration: query.extraRightsGainDuration
|
||||||
|
? parseInt(query.extraRightsGainDuration)
|
||||||
|
: hint.extraRightsGainDuration !== undefined
|
||||||
|
? parseInt(hint.extraRightsGainDuration)
|
||||||
|
: undefined,
|
||||||
|
nextRightsGainDuration: query.nextRightsGainDuration
|
||||||
|
? parseInt(query.nextRightsGainDuration)
|
||||||
|
: hint.nextRightsGainDuration !== undefined
|
||||||
|
? parseInt(hint.nextRightsGainDuration)
|
||||||
|
: undefined,
|
||||||
|
|
||||||
|
// 权益类型(含 RIGHTS_GAIN_TYPE_CURRENT_DAY 等取值)
|
||||||
|
rightsGainType: query.rightsGainType
|
||||||
|
? parseInt(query.rightsGainType)
|
||||||
|
: hint.rightsGainType !== undefined
|
||||||
|
? parseInt(hint.rightsGainType)
|
||||||
|
: undefined,
|
||||||
|
|
||||||
|
// 权益时长
|
||||||
|
rightsGainDuration: query.rightsGainDuration
|
||||||
|
? parseInt(query.rightsGainDuration)
|
||||||
|
: hint.rightsGainDuration !== undefined
|
||||||
|
? parseInt(hint.rightsGainDuration)
|
||||||
|
: undefined,
|
||||||
|
|
||||||
|
// 领取步骤(UNGAIN/GAINING/GAIN_FINISHED 对应的值)
|
||||||
|
gainMethodStep: query.gainMethodStep
|
||||||
|
? parseInt(query.gainMethodStep)
|
||||||
|
: undefined,
|
||||||
|
|
||||||
|
// 通用权益信息(ad.generalRightsInfo 序列化 JSON)
|
||||||
|
generalRightsInfo,
|
||||||
|
|
||||||
|
// 权益扩展信息
|
||||||
|
rightsExtJson: query.rightsExtJson || hint.rightsExtJson || undefined,
|
||||||
|
|
||||||
|
// 应用信息(下载类广告)
|
||||||
|
appInfo: query.appInfo ? JSON.parse(query.appInfo) : undefined,
|
||||||
|
|
||||||
|
// 广告上下文(ad.adLogId.contextInfo,客户端真实来源)
|
||||||
|
contextInfo,
|
||||||
|
|
||||||
|
// 应用是否已安装(下载类广告)
|
||||||
|
installed: query.installed ? parseInt(query.installed) : undefined,
|
||||||
|
|
||||||
|
// 嗅探时间:逆向确认(classes5.dex AdDSLUtils.requestRightGain):
|
||||||
|
// rightsGainMethod==3(曝光+下载) 或 6(LAXIN) 时传 currentTimeMillis
|
||||||
|
sniffTime:
|
||||||
|
rightsGainMethod === 3 || rightsGainMethod === 6
|
||||||
|
? query.sniffTime
|
||||||
|
? parseInt(query.sniffTime)
|
||||||
|
: time
|
||||||
|
: undefined,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理 undefined 字段
|
||||||
|
Object.keys(rightsParam).forEach((key) => {
|
||||||
|
if (rightsParam[key] === undefined) delete rightsParam[key]
|
||||||
|
})
|
||||||
|
|
||||||
|
// 将参数序列化为 reqParam 字符串 (与原生源码一致)
|
||||||
|
const data = {
|
||||||
|
reqParam: JSON.stringify(rightsParam),
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await request(
|
||||||
|
`/api/ad/listening/rights/gain`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'xeapi', 'v3'),
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
code: 200,
|
||||||
|
data: res.body,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
// 获取关注歌手的新歌曲和 MV
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
startTimestamp: query.startTimestamp || query.before || Date.now(),
|
||||||
|
sourceType: query.sourceType || 1,
|
||||||
|
limit: query.limit || 10,
|
||||||
|
firstRequest: query.firstRequest ?? true,
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
`/api/sub/artist/new/works/song-mv/list/v2`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'eapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
// 获取所有关注歌手最近的 50 首新歌
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
return request(
|
||||||
|
`/api/sub/artist/new/works/song/playall`,
|
||||||
|
{},
|
||||||
|
createOption(query, 'eapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
// 发送安全验证码
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
ctcode: query.ctcode || '86',
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
`/api/sms/captcha/safe/sent`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'eapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
// 发送验证码
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
ctcode: query.ctcode || '86',
|
||||||
|
secrete: 'music_middleuser_pclogin',
|
||||||
|
cellphone: query.phone,
|
||||||
|
scene: '0',
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
`/api/middle/captcha/sent/v1`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'eapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -6,5 +6,9 @@ module.exports = (query, request) => {
|
|||||||
cellphone: query.phone,
|
cellphone: query.phone,
|
||||||
countrycode: query.countrycode,
|
countrycode: query.countrycode,
|
||||||
}
|
}
|
||||||
return request(`/api/cellphone/existence/check`, data, createOption(query))
|
return request(
|
||||||
|
`/api/cellphone/existence/check`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'eapi'),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// 获取指定维度音乐排行榜详情
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
chartCode: query.chartCode,
|
||||||
|
targetId: query.targetId,
|
||||||
|
targetType: query.targetType,
|
||||||
|
}
|
||||||
|
return request(`/api/chart/detail`, data, createOption(query))
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// 获取指定维度音乐排行榜列表
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
chartCode: query.chartCode,
|
||||||
|
targetId: query.targetId,
|
||||||
|
targetType: query.targetType,
|
||||||
|
}
|
||||||
|
return request(`/api/chart/song/detail`, data, createOption(query))
|
||||||
|
}
|
||||||
+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'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
const {
|
||||||
|
eapiResDecrypt,
|
||||||
|
eapiReqDecrypt,
|
||||||
|
aesDecrypt,
|
||||||
|
xeapiResDecrypt,
|
||||||
|
} = require('../util/crypto')
|
||||||
|
const CryptoJS = require('crypto-js')
|
||||||
|
|
||||||
|
const linuxapiKey = 'rFgB&h#%2?^eDg:Q'
|
||||||
|
|
||||||
|
module.exports = async (query, request) => {
|
||||||
|
const crypto = query.crypto || 'eapi'
|
||||||
|
const data = query.data || query.hexString || ''
|
||||||
|
const isReq = query.isReq !== 'false'
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return {
|
||||||
|
status: 400,
|
||||||
|
body: { code: 400, message: 'data is required' },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let result
|
||||||
|
switch (crypto) {
|
||||||
|
case 'eapi': {
|
||||||
|
const pureHex = data.replace(/\s/g, '')
|
||||||
|
result = isReq ? eapiReqDecrypt(pureHex) : eapiResDecrypt(pureHex)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'weapi': {
|
||||||
|
if (isReq) {
|
||||||
|
return {
|
||||||
|
status: 400,
|
||||||
|
body: {
|
||||||
|
code: 400,
|
||||||
|
message:
|
||||||
|
'weapi 请求解密需要 RSA 私钥,暂不支持;仅支持 weapi 返回数据解密(e_r=true 时与 eapi 相同)',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const pureHex = data.replace(/\s/g, '')
|
||||||
|
result = eapiResDecrypt(pureHex)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'linuxapi': {
|
||||||
|
if (isReq) {
|
||||||
|
const pureHex = data.replace(/\s/g, '')
|
||||||
|
const decrypted = aesDecrypt(pureHex, 'ecb', linuxapiKey, '', 'hex')
|
||||||
|
result = JSON.parse(decrypted.toString(CryptoJS.enc.Utf8))
|
||||||
|
} else {
|
||||||
|
result = typeof data === 'string' ? JSON.parse(data) : data
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'xeapi': {
|
||||||
|
if (isReq) {
|
||||||
|
return {
|
||||||
|
status: 400,
|
||||||
|
body: {
|
||||||
|
code: 400,
|
||||||
|
message:
|
||||||
|
'xeapi 请求解密涉及 X25519 ECDH 密钥交换,流程复杂,暂不支持;仅支持 xeapi 返回数据解密',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const buf = Buffer.from(data, 'base64')
|
||||||
|
result = xeapiResDecrypt(buf)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'api': {
|
||||||
|
result = typeof data === 'string' ? JSON.parse(data) : data
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
status: 400,
|
||||||
|
body: { code: 400, message: `未知加密方式: ${crypto}` },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
body: { code: 200, data: result },
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
status: 400,
|
||||||
|
body: { code: 400, message: `解密失败: ${error.message}` },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,9 @@
|
|||||||
|
// 多级行政区划数据获取接口
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
bizCode: query.bizCode || '',
|
||||||
|
}
|
||||||
|
return request(`/api/lbs/city/code`, data, createOption(query))
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
// 红心与取消红心歌曲- v1
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
query.like = query.like == 'false' ? false : true
|
||||||
|
const data = {
|
||||||
|
alg: 'itembased',
|
||||||
|
trackId: query.id,
|
||||||
|
like: query.like,
|
||||||
|
time: '3',
|
||||||
|
}
|
||||||
|
return request(`/api/v1/radio/like`, data, createOption(query, 'xeapi', 'v3'))
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
// 听歌足迹 - 歌曲播放排行 (Top20)
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
return request(
|
||||||
|
`/api/content/activity/listen/data/song/play/rank`,
|
||||||
|
{
|
||||||
|
type: query.type || 'month', //周 week 月 month
|
||||||
|
endTime: query.endTime, // 不填就是本周/月的
|
||||||
|
},
|
||||||
|
createOption(query),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ module.exports = async (query, request) => {
|
|||||||
? query.captcha
|
? query.captcha
|
||||||
: query.md5_password || CryptoJS.MD5(query.password).toString(),
|
: query.md5_password || CryptoJS.MD5(query.password).toString(),
|
||||||
remember: 'true',
|
remember: 'true',
|
||||||
|
secureCaptcha: query.sca || '',
|
||||||
}
|
}
|
||||||
let result = await request(
|
let result = await request(
|
||||||
`/api/w/login/cellphone`,
|
`/api/w/login/cellphone`,
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
// 云小编每日抽奖
|
||||||
|
//
|
||||||
|
// activityId:
|
||||||
|
// 默认 6501202
|
||||||
|
//
|
||||||
|
// drawCount:
|
||||||
|
// 默认 1
|
||||||
|
//
|
||||||
|
// checkToken:
|
||||||
|
// 易盾反作弊 Token
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
activityId: query.activityId || '6501202',
|
||||||
|
drawCount: query.drawCount || '1',
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
`/api/middle/play/do/lottery`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'eapi', 'v2'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
// 云小编抽奖剩余次数查询
|
||||||
|
//
|
||||||
|
// activityId:
|
||||||
|
// 默认 6501202
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
activityId: query.activityId || '6501202',
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
`/api/middle/play/lottery/remain/chance`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'eapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,8 +1,5 @@
|
|||||||
// 私人FM - 模式选择
|
// 私人FM - 模式选择
|
||||||
|
|
||||||
// aidj, DEFAULT, FAMILIAR, EXPLORE, SCENE_RCMD ( EXERCISE, FOCUS, NIGHT_EMO )
|
|
||||||
// 来不及解释这几个了
|
|
||||||
|
|
||||||
const createOption = require('../util/option.js')
|
const createOption = require('../util/option.js')
|
||||||
module.exports = (query, request) => {
|
module.exports = (query, request) => {
|
||||||
const data = {
|
const data = {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
// 私信和通知接口
|
// 私信和通知接口
|
||||||
|
|
||||||
const createOption = require('../util/option.js')
|
const createOption = require('../util/option.js')
|
||||||
module.exports = (query, request) => {
|
module.exports = (query, request) => {
|
||||||
const data = {}
|
const data = {}
|
||||||
|
|||||||
@@ -9,6 +9,6 @@ module.exports = (query, request) => {
|
|||||||
? { checkToken: query.checkToken || APP_CONF.checkToken }
|
? { checkToken: query.checkToken || APP_CONF.checkToken }
|
||||||
: {}),
|
: {}),
|
||||||
}
|
}
|
||||||
query.checkToken = true // 强制开启checkToken
|
query.checkToken = 'v2' // 强制开启checkToken
|
||||||
return request(`/api/playlist/${path}`, data, createOption(query, 'eapi'))
|
return request(`/api/playlist/${path}`, data, createOption(query, 'eapi'))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// 每日风格推荐场景配置列表获取
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {}
|
||||||
|
return request(
|
||||||
|
`/api/homepage/daily/song/config/get`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'xeapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
// 每日风格推荐场景配置设置
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
tags: JSON.stringify({
|
||||||
|
tagIds: String(query.tags).split(',').map(Number),
|
||||||
|
categoryId: Number(query.category),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
console.log(data)
|
||||||
|
return request(
|
||||||
|
`/api/homepage/daily/song/tag/save`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'xeapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// 每日推荐风格歌曲
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {}
|
||||||
|
return request(
|
||||||
|
`/api/homepage/category/daily/song/list`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'xeapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
// 每日推荐歌曲 - v1
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
ispush: query.ispush || false,
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
`/api/v3/discovery/recommend/songs`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'xeapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// 获取游客cookie
|
||||||
|
|
||||||
const CryptoJS = require('crypto-js')
|
const CryptoJS = require('crypto-js')
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
@@ -37,7 +39,7 @@ module.exports = async (query, request) => {
|
|||||||
let result = await request(
|
let result = await request(
|
||||||
`/api/register/anonimous`,
|
`/api/register/anonimous`,
|
||||||
data,
|
data,
|
||||||
createOption(query, 'weapi'),
|
createOption(query, 'xeapi'),
|
||||||
)
|
)
|
||||||
if (result.body.code === 200) {
|
if (result.body.code === 200) {
|
||||||
result = {
|
result = {
|
||||||
|
|||||||
@@ -0,0 +1,158 @@
|
|||||||
|
// 易盾反作弊 Token 注册端点
|
||||||
|
// 通过易盾官方 Watchman SDK(Web 版,跑在 jsdom 模拟的浏览器环境里)
|
||||||
|
// 实时调用 getToken(businessId) 获取反作弊 token,供后续带 checkToken 的请求使用
|
||||||
|
//
|
||||||
|
// GET /register/checktoken/v2 → 实时获取新 token(不缓存)
|
||||||
|
// POST /register/checktoken/v2 → 实时获取新 token
|
||||||
|
//
|
||||||
|
// 注意:每次获取都不缓存,模拟真实客户端每次请求使用新鲜 token,
|
||||||
|
// 避免反作弊 token 复用触发风控。
|
||||||
|
//
|
||||||
|
// 注:jsdom 固定用 v24(engines >=18),其依赖链为纯 CJS,可被 pkg 静态
|
||||||
|
// 分析自动打包;v30+ 引入纯 ESM 依赖且要求 Node >=22,无法在 CI(18-24) 与
|
||||||
|
// pkg(node18) 目标下运行。
|
||||||
|
//
|
||||||
|
const { JSDOM, VirtualConsole } = require('jsdom')
|
||||||
|
const { default: axios } = require('axios')
|
||||||
|
const { APP_CONF } = require('../util/config.json')
|
||||||
|
const logger = require('../util/logger')
|
||||||
|
|
||||||
|
// 网易云音乐在易盾的 productNumber 与 businessId
|
||||||
|
const PRODUCT_NUMBER = 'YD00000558929251'
|
||||||
|
const BUSINESS_ID = 'bd5d2f973ef74cd2a61325a412ae54d9'
|
||||||
|
const TOOL_JS_URL = `${APP_CONF.dunStaticDomain}/tool.min.js`
|
||||||
|
|
||||||
|
// 最小 HTML 外壳,模拟网页环境
|
||||||
|
const HTML =
|
||||||
|
'<!doctype html><html><head><meta charset="UTF-8"></head><body></body></html>'
|
||||||
|
|
||||||
|
let toolJs = ''
|
||||||
|
let wm = null // Watchman 实例(进程内复用,可反复 getToken)
|
||||||
|
let dom = null // jsdom 实例(失败时 close 释放活动句柄,避免泄漏)
|
||||||
|
let initPromise = null
|
||||||
|
|
||||||
|
// 获取 tool.min.js(内存缓存,避免每次初始化重复下载)
|
||||||
|
async function getToolJs() {
|
||||||
|
if (toolJs) return toolJs
|
||||||
|
const res = await axios.get(TOOL_JS_URL, { timeout: 10000 })
|
||||||
|
toolJs = String(res.data)
|
||||||
|
return toolJs
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化 Watchman(进程内只初始化一次,实例可反复 getToken)
|
||||||
|
async function ensureWatchman() {
|
||||||
|
if (wm) return wm
|
||||||
|
if (initPromise) return initPromise
|
||||||
|
|
||||||
|
initPromise = (async () => {
|
||||||
|
const js = await getToolJs()
|
||||||
|
const virtualConsole = new VirtualConsole()
|
||||||
|
virtualConsole.on('jsdomError', () => {})
|
||||||
|
dom = new JSDOM(HTML, {
|
||||||
|
url: 'https://music.163.com/',
|
||||||
|
referrer: 'https://music.163.com/',
|
||||||
|
contentType: 'text/html',
|
||||||
|
runScripts: 'dangerously',
|
||||||
|
resources: 'usable', // 允许动态加载 watchman.min.js / JSONP
|
||||||
|
pretendToBeVisual: true,
|
||||||
|
virtualConsole,
|
||||||
|
beforeParse(window) {
|
||||||
|
// 抹掉 headless 特征,避免易盾风控误判
|
||||||
|
Object.defineProperty(window.navigator, 'webdriver', {
|
||||||
|
get: () => undefined,
|
||||||
|
})
|
||||||
|
window.chrome = { runtime: {} }
|
||||||
|
window.navigator.languages = ['zh-CN', 'zh']
|
||||||
|
window.navigator.plugins = [1, 2, 3, 4, 5]
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const script = dom.window.document.createElement('script')
|
||||||
|
script.textContent = js
|
||||||
|
dom.window.document.body.appendChild(script)
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let settled = false
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
if (settled) return
|
||||||
|
settled = true
|
||||||
|
reject(new Error('watchman 初始化超时'))
|
||||||
|
}, 15000)
|
||||||
|
dom.window.initWatchman({
|
||||||
|
auto: true,
|
||||||
|
productNumber: PRODUCT_NUMBER,
|
||||||
|
onload(instance) {
|
||||||
|
if (settled) return
|
||||||
|
settled = true
|
||||||
|
clearTimeout(timer)
|
||||||
|
wm = instance
|
||||||
|
resolve(instance)
|
||||||
|
},
|
||||||
|
onerror(...args) {
|
||||||
|
if (settled) return
|
||||||
|
settled = true
|
||||||
|
clearTimeout(timer)
|
||||||
|
reject(new Error('watchman 初始化失败'))
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})()
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await initPromise
|
||||||
|
} catch (e) {
|
||||||
|
// 失败路径:关闭 jsdom 释放 rAF/子资源/定时器等活动句柄,防止每次失败泄漏约 30MB
|
||||||
|
if (dom) {
|
||||||
|
dom.window.close()
|
||||||
|
dom = null
|
||||||
|
}
|
||||||
|
initPromise = null
|
||||||
|
wm = null
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取新 token
|
||||||
|
async function fetchToken() {
|
||||||
|
const instance = await ensureWatchman()
|
||||||
|
const raw = instance.getInstance()
|
||||||
|
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
const timer = setTimeout(() => resolve(), 15000)
|
||||||
|
raw.I(() => {
|
||||||
|
clearTimeout(timer)
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const timer = setTimeout(() => resolve(''), 15000)
|
||||||
|
instance.getToken(BUSINESS_ID, (tk) => {
|
||||||
|
clearTimeout(timer)
|
||||||
|
resolve(typeof tk === 'string' ? tk : '')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 端点处理:每次实时获取新 token
|
||||||
|
module.exports = async () => {
|
||||||
|
let token = ''
|
||||||
|
try {
|
||||||
|
token = await fetchToken()
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn('[checkToken v2]', e.message)
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
body: { code: 200, token, registered: !!token },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 给 request.js 读取用:每次调用实时获取新 token,不缓存
|
||||||
|
module.exports.getToken = async () => {
|
||||||
|
try {
|
||||||
|
return await fetchToken()
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn('[checkToken v2]', e.message)
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
// 易盾反作弊 Token 注册端点
|
||||||
|
// 调用后获取实时 token 并存入共享存储,供后续带 checkToken 的请求使用
|
||||||
|
//
|
||||||
|
// GET /register/checktoken/v3 → 实时获取新 token(不缓存)
|
||||||
|
// POST /register/checktoken/v3 → 实时获取新 token
|
||||||
|
//
|
||||||
|
// 注意:每次获取都不缓存,模拟真实客户端每次请求使用新鲜 token,
|
||||||
|
// 避免反作弊 token 复用触发风控。
|
||||||
|
//
|
||||||
|
const { default: axios } = require('axios')
|
||||||
|
const { APP_CONF } = require('../util/config.json')
|
||||||
|
|
||||||
|
const URL = APP_CONF.dunDomainV3 + '/v3/b?pn=YD00000558929251'
|
||||||
|
|
||||||
|
async function fetch() {
|
||||||
|
const res = await axios.get(URL, { timeout: 10000 })
|
||||||
|
const body = String(res.data)
|
||||||
|
const m = body.match(/null\(\[(\d+),\d+,\"([^\"]+)\"\]\)/)
|
||||||
|
if (m && m[1] === '200') return m[2]
|
||||||
|
throw new Error('易盾返回异常: ' + body.substring(0, 100))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 端点处理:每次实时获取新 token
|
||||||
|
module.exports = async () => {
|
||||||
|
let token = ''
|
||||||
|
try {
|
||||||
|
token = await fetch()
|
||||||
|
} catch (e) {
|
||||||
|
// token 获取失败时返回空,由调用方决定是否重试
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
body: { code: 200, token, registered: !!token },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 给 request.js 读取用:每次调用实时获取新 token,不缓存
|
||||||
|
module.exports.getToken = async () => {
|
||||||
|
try {
|
||||||
|
return await fetch()
|
||||||
|
} catch (e) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
const { default: axios } = require('axios')
|
||||||
|
const { APP_CONF } = require('../util/config.json')
|
||||||
|
const { saveNeapiKey } = require('../util/neapiKey')
|
||||||
|
const { decodeConfig } = require('../util/neapiConfig')
|
||||||
|
|
||||||
|
const USER_AGENT =
|
||||||
|
'NeteaseMusic/9.5.61.260802021928(9005061);Dalvik/2.1.0 (Linux; U; Android 12; HBN-AL00 Build/cd737a2.0)'
|
||||||
|
|
||||||
|
module.exports = async (query, request) => {
|
||||||
|
const version = Number(query.version) || 0
|
||||||
|
const behavior = query.behavior === 'update' ? 'update' : 'open'
|
||||||
|
|
||||||
|
const res = await axios({
|
||||||
|
method: 'GET',
|
||||||
|
url: APP_CONF.apiDomain + '/api/gorilla/algorithm/record',
|
||||||
|
params: { version, behavior },
|
||||||
|
headers: { 'User-Agent': USER_AGENT },
|
||||||
|
proxy: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!res.data || res.data.code !== 200 || !res.data.data) {
|
||||||
|
throw new Error('neapi config request failed')
|
||||||
|
}
|
||||||
|
|
||||||
|
// file 为空表示本地已是最新,无需更新
|
||||||
|
const { file, circleTime } = res.data.data
|
||||||
|
const config = file
|
||||||
|
? saveNeapiKey(decodeConfig(Buffer.from(file, 'base64')), circleTime)
|
||||||
|
: null
|
||||||
|
|
||||||
|
// 只回版本与密钥偏移,密钥本身留在本地缓存
|
||||||
|
const keyOffsets = {}
|
||||||
|
if (config) {
|
||||||
|
keyOffsets.encrypt = config.keyBlob.indexOf(config.encryptKey)
|
||||||
|
keyOffsets.decrypt = config.keyBlob.indexOf(config.decryptKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
version: config ? config.version : res.data.data.version,
|
||||||
|
updated: Boolean(config),
|
||||||
|
circleTime,
|
||||||
|
keyOffsets,
|
||||||
|
},
|
||||||
|
cookie: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
const { default: axios } = require('axios')
|
||||||
|
const encrypt = require('../util/crypto')
|
||||||
|
const { APP_CONF } = require('../util/config.json')
|
||||||
|
|
||||||
|
const generateNonce = () => {
|
||||||
|
let nonce = ''
|
||||||
|
for (let i = 0; i < 16; i++) {
|
||||||
|
nonce += Math.floor(Math.random() * 10).toString()
|
||||||
|
}
|
||||||
|
return nonce
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = async (query, request) => {
|
||||||
|
const nonce = generateNonce()
|
||||||
|
const timestamp = String(Date.now())
|
||||||
|
const deviceId = query.deviceId || global.deviceId || ''
|
||||||
|
const currentKeyVersion = query.currentKeyVersion || ''
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
appVersion: '9.5.61',
|
||||||
|
currentKeyVersion,
|
||||||
|
deviceId,
|
||||||
|
nonce,
|
||||||
|
os: 'android',
|
||||||
|
requestType: 'active',
|
||||||
|
signature: encrypt.xeapiSign(timestamp, nonce),
|
||||||
|
t1: '',
|
||||||
|
t2: '',
|
||||||
|
timestamp,
|
||||||
|
uid: '',
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await axios({
|
||||||
|
method: 'POST',
|
||||||
|
url: APP_CONF.apiDomain + '/api/bsr/sk/get',
|
||||||
|
headers: {
|
||||||
|
'User-Agent':
|
||||||
|
'NeteaseMusic/9.5.61.260802021928(9005061);Dalvik/2.1.0 (Linux; U; Android 12; HBN-AL00 Build/cd737a2.0)',
|
||||||
|
Cookie: deviceId ? `deviceId=${encodeURIComponent(deviceId)}` : '',
|
||||||
|
},
|
||||||
|
data: new URLSearchParams(data).toString(),
|
||||||
|
proxy: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (
|
||||||
|
!res.data ||
|
||||||
|
res.data.code !== 200 ||
|
||||||
|
!res.data.data ||
|
||||||
|
!res.data.data.encryptedData
|
||||||
|
) {
|
||||||
|
throw new Error('xeapi public key request failed')
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!res.data.data.signature ||
|
||||||
|
encrypt.xeapiSign(res.data.data.timestamp, nonce) !==
|
||||||
|
res.data.data.signature
|
||||||
|
) {
|
||||||
|
throw new Error('xeapi public key response signature mismatch')
|
||||||
|
}
|
||||||
|
|
||||||
|
const publicKey = encrypt.xeapiDecryptPublicKey(res.data.data.encryptedData)
|
||||||
|
if (!publicKey.sk) {
|
||||||
|
throw new Error('xeapi public key response missing sk')
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
...publicKey,
|
||||||
|
deviceId,
|
||||||
|
},
|
||||||
|
cookie: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
// 提交歌曲播放状态
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
const generateSessionId = () =>
|
||||||
|
Array.from(
|
||||||
|
{ length: 12 },
|
||||||
|
() =>
|
||||||
|
'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'[Math.floor(Math.random() * 36)],
|
||||||
|
).join('')
|
||||||
|
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
sessionId,
|
||||||
|
progress = 0,
|
||||||
|
playMode = 'list_loop',
|
||||||
|
type = 'song',
|
||||||
|
} = query
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
return Promise.reject({
|
||||||
|
status: 400,
|
||||||
|
body: {
|
||||||
|
code: 400,
|
||||||
|
msg: '缺少必要参数:id',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const playStateSubmitReq = JSON.stringify({
|
||||||
|
resource: {
|
||||||
|
id: String(id),
|
||||||
|
type: type,
|
||||||
|
},
|
||||||
|
progress: Number(progress) || 0,
|
||||||
|
sessionId: sessionId || generateSessionId(),
|
||||||
|
playMode: playMode,
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
playStateSubmitReq: playStateSubmitReq,
|
||||||
|
}
|
||||||
|
|
||||||
|
return request(
|
||||||
|
'/api/relay/play/state/submit',
|
||||||
|
data,
|
||||||
|
createOption(query, 'weapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
// 云小编领取任务积分
|
||||||
|
//
|
||||||
|
// activityId:
|
||||||
|
// 调用 rep/ugc/activity/get 获取
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
activityId: query.activityId || '5001',
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
`/api/rep/ugc/activity/collect`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'eapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
// 云小编活动信息
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
return request(`/api/rep/ugc/activity/get`, {}, createOption(query, 'eapi'))
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
// 云小编考试状态
|
||||||
|
//
|
||||||
|
// examType:
|
||||||
|
// 1. 歌曲曲风审核: musicalStyleEnter
|
||||||
|
// 2. 歌曲语种审核: languageEnter
|
||||||
|
// 3. 歌曲原唱审核: oriSingerEnter
|
||||||
|
// 4. 情绪标签审核: emotionEnter
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
if (!query.examType)
|
||||||
|
return Promise.reject({
|
||||||
|
status: 400,
|
||||||
|
body: {
|
||||||
|
code: 400,
|
||||||
|
message: '参数不足',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const data = {
|
||||||
|
examType: query.examType,
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
'/api/rep/ugc/exam/info/get',
|
||||||
|
data,
|
||||||
|
createOption(query, 'eapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
// 云小编考试取题
|
||||||
|
//
|
||||||
|
// examType:
|
||||||
|
// 1. 歌曲曲风审核: musicalStyleEnter
|
||||||
|
// 2. 歌曲语种审核: languageEnter
|
||||||
|
// 3. 歌曲原唱审核: oriSingerEnter
|
||||||
|
// 4. 情绪标签审核: emotionEnter
|
||||||
|
//
|
||||||
|
// taskId:
|
||||||
|
// 首次调用 rep/ugc/exam/start 获取,之后调用 rep/ugc/exam/info/get 获取
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
if (!query.examType || !query.taskId)
|
||||||
|
return Promise.reject({
|
||||||
|
status: 400,
|
||||||
|
body: {
|
||||||
|
code: 400,
|
||||||
|
message: '参数不足',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const data = {
|
||||||
|
examType: query.examType,
|
||||||
|
taskId: query.taskId,
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
'/api/rep/ugc/exam/question/single/get',
|
||||||
|
data,
|
||||||
|
createOption(query, 'eapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
// 云小编考试结果
|
||||||
|
//
|
||||||
|
// examType:
|
||||||
|
// 1. 歌曲曲风审核: musicalStyleEnter
|
||||||
|
// 2. 歌曲语种审核: languageEnter
|
||||||
|
// 3. 歌曲原唱审核: oriSingerEnter
|
||||||
|
// 4. 情绪标签审核: emotionEnter
|
||||||
|
//
|
||||||
|
// taskId:
|
||||||
|
// 首次调用 rep/ugc/exam/start 获取,之后调用 rep/ugc/exam/info/get 获取
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
if (!query.examType || !query.taskId)
|
||||||
|
return Promise.reject({
|
||||||
|
status: 400,
|
||||||
|
body: {
|
||||||
|
code: 400,
|
||||||
|
message: '参数不足',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const data = {
|
||||||
|
examType: query.examType,
|
||||||
|
taskId: query.taskId,
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
'/api/rep/ugc/exam/result/get',
|
||||||
|
data,
|
||||||
|
createOption(query, 'eapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
// 云小编考试开始
|
||||||
|
//
|
||||||
|
// examType:
|
||||||
|
// 1. 歌曲曲风审核: musicalStyleEnter
|
||||||
|
// 2. 歌曲语种审核: languageEnter
|
||||||
|
// 3. 歌曲原唱审核: oriSingerEnter
|
||||||
|
// 4. 情绪标签审核: emotionEnter
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
if (!query.examType)
|
||||||
|
return Promise.reject({
|
||||||
|
status: 400,
|
||||||
|
body: {
|
||||||
|
code: 400,
|
||||||
|
message: '参数不足',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const data = {
|
||||||
|
examType: query.examType,
|
||||||
|
}
|
||||||
|
return request('/api/rep/ugc/exam/start', data, createOption(query, 'eapi'))
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
// 云小编考试提交
|
||||||
|
//
|
||||||
|
// examType:
|
||||||
|
// 1. 歌曲曲风审核: musicalStyleEnter
|
||||||
|
// 2. 歌曲语种审核: languageEnter
|
||||||
|
// 3. 歌曲原唱审核: oriSingerEnter
|
||||||
|
// 4. 情绪标签审核: emotionEnter
|
||||||
|
//
|
||||||
|
// taskId:
|
||||||
|
// 首次调用 rep/ugc/exam/start 获取,之后调用 rep/ugc/exam/info/get 获取
|
||||||
|
//
|
||||||
|
// questionId:
|
||||||
|
// 调用 rep/ugc/exam/question/single/get 获取
|
||||||
|
//
|
||||||
|
// answer:
|
||||||
|
// A 对, B 错
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
if (!query.examType || !query.taskId || !query.questionId || !query.answer)
|
||||||
|
return Promise.reject({
|
||||||
|
status: 400,
|
||||||
|
body: {
|
||||||
|
code: 400,
|
||||||
|
message: '参数不足',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const data = {
|
||||||
|
examType: query.examType,
|
||||||
|
taskId: query.taskId,
|
||||||
|
questionId: query.questionId,
|
||||||
|
answer: query.answer,
|
||||||
|
}
|
||||||
|
return request('/api/rep/ugc/exam/submit', data, createOption(query, 'eapi'))
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
// 云小编领取一日会员
|
||||||
|
//
|
||||||
|
// 注:前提条件见 rep/ugc/user/vip
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
activityId: query.activityId || '5001',
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
`/api/rep/ugc/user/collect-vip`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'eapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
// 云小编获取用户详情
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
return request(`/api/rep/ugc/user/get`, {}, createOption(query, 'eapi'))
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
// 云小编每日签到
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
return request(`/api/rep/ugc/user/sign`, {}, createOption(query, 'eapi'))
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
// 云小编查询会员任务状态
|
||||||
|
//
|
||||||
|
// 状态 (data.status)
|
||||||
|
// 10: 用户积分达50,可免费领取1日黑胶会员
|
||||||
|
// 20: 用户积分已达50,可免费领取1日黑胶会员
|
||||||
|
// 30: 已领取1日黑胶会员,明天再来吧~
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
return request(`/api/rep/ugc/user/vip`, {}, createOption(query, 'eapi'))
|
||||||
|
}
|
||||||
+57
-5
@@ -1,8 +1,42 @@
|
|||||||
// 听歌打卡
|
// 听歌打卡
|
||||||
|
|
||||||
const createOption = require('../util/option.js')
|
const createOption = require('../util/option.js')
|
||||||
module.exports = (query, request) => {
|
const { APP_CONF } = require('../util/config.json')
|
||||||
const data = {
|
const DOMAIN = APP_CONF.clDomian
|
||||||
|
module.exports = async (query, request) => {
|
||||||
|
// 注入 os=osx 的 cookie
|
||||||
|
let cookie = query.cookie || ''
|
||||||
|
if (typeof cookie === 'object') {
|
||||||
|
cookie = Object.assign({ os: 'osx' }, cookie)
|
||||||
|
} else if (typeof cookie === 'string') {
|
||||||
|
if (cookie.indexOf('os=') > -1) {
|
||||||
|
cookie = cookie.replace(/os=[^;]+/g, 'os=osx')
|
||||||
|
} else {
|
||||||
|
cookie = cookie + '; os=osx'
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cookie = 'os=osx'
|
||||||
|
}
|
||||||
|
query.cookie = cookie
|
||||||
|
|
||||||
|
// 1) startplay → 进「最近播放」
|
||||||
|
const startplayData = {
|
||||||
|
logs: JSON.stringify([
|
||||||
|
{
|
||||||
|
action: 'startplay',
|
||||||
|
json: {
|
||||||
|
id: query.id,
|
||||||
|
type: 'song',
|
||||||
|
mainsite: '1',
|
||||||
|
mainsiteWeb: '1',
|
||||||
|
content: `id=${query.sourceid}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) play → 涨「听歌排行」计数
|
||||||
|
const playData = {
|
||||||
logs: JSON.stringify([
|
logs: JSON.stringify([
|
||||||
{
|
{
|
||||||
action: 'play',
|
action: 'play',
|
||||||
@@ -15,12 +49,30 @@ module.exports = (query, request) => {
|
|||||||
type: 'song',
|
type: 'song',
|
||||||
wifi: 0,
|
wifi: 0,
|
||||||
source: 'list',
|
source: 'list',
|
||||||
mainsite: 1,
|
mainsite: '1',
|
||||||
content: '',
|
mainsiteWeb: '1',
|
||||||
|
content: `id=${query.sourceid}`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
}
|
}
|
||||||
|
|
||||||
return request(`/api/feedback/weblog`, data, createOption(query, 'weapi'))
|
const option = createOption(query, 'eapi')
|
||||||
|
option.domain = DOMAIN
|
||||||
|
|
||||||
|
// 发送两次请求
|
||||||
|
const res1 = await request(`/api/feedback/weblog`, startplayData, option)
|
||||||
|
const res2 = await request(`/api/feedback/weblog`, playData, option)
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
code: 200,
|
||||||
|
data: 'success',
|
||||||
|
details: {
|
||||||
|
startplay: res1.body,
|
||||||
|
play: res2.body,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,128 @@
|
|||||||
|
// 听歌打卡 - NCBL 加密版 (仿桌面客户端 PLV/PLD 上报)
|
||||||
|
// 复制自 https://github.com/folltoshe/netease-report-listen-song 的 PC 端日志上报方式
|
||||||
|
//
|
||||||
|
// PLV 和 PLD 分两次独立上传
|
||||||
|
|
||||||
|
const {
|
||||||
|
buildPlv,
|
||||||
|
buildPld,
|
||||||
|
buildRecords,
|
||||||
|
extractContext,
|
||||||
|
parseCookie,
|
||||||
|
buildCookieStr,
|
||||||
|
buildMetaJson,
|
||||||
|
doUpload,
|
||||||
|
} = require('../util/ncbl')
|
||||||
|
|
||||||
|
module.exports = async (query, request) => {
|
||||||
|
// --- 参数校验 ---
|
||||||
|
const songId = Number(query.id)
|
||||||
|
if (!songId || isNaN(songId)) {
|
||||||
|
return { status: 400, body: { code: 400, msg: '缺少有效的 id (歌曲ID)' } }
|
||||||
|
}
|
||||||
|
const playTime = Number(query.time)
|
||||||
|
if (isNaN(playTime) || playTime <= 0) {
|
||||||
|
return {
|
||||||
|
status: 400,
|
||||||
|
body: { code: 400, msg: '缺少有效的 time (播放时长)' },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const totalTime = Number(query.total) || playTime
|
||||||
|
const sourceId = String(query.sourceid || query.sourceId || '')
|
||||||
|
const sourceName = query.source || 'list'
|
||||||
|
|
||||||
|
// --- 解析认证上下文 ---
|
||||||
|
const rawCookie = query.cookie || ''
|
||||||
|
const cookieObj = parseCookie(rawCookie)
|
||||||
|
cookieObj.os = 'pc'
|
||||||
|
const ctx = extractContext(cookieObj)
|
||||||
|
|
||||||
|
// 兜底取 token
|
||||||
|
if (!ctx.auth.token && rawCookie) {
|
||||||
|
const parsed =
|
||||||
|
typeof rawCookie === 'string' ? parseCookie(rawCookie) : rawCookie
|
||||||
|
ctx.auth.token = parsed.MUSIC_U || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ctx.auth.token) {
|
||||||
|
return { status: 401, body: { code: 401, msg: '缺少 MUSIC_U 鉴权令牌' } }
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 构建歌曲和来源 ---
|
||||||
|
const song = {
|
||||||
|
id: songId,
|
||||||
|
name: query.name || '',
|
||||||
|
artist: query.artist || '',
|
||||||
|
bitrate: Number(query.bitrate) || 320,
|
||||||
|
level: query.level || 'exhigh',
|
||||||
|
vip: query.vip === 'true' || query.vip === true,
|
||||||
|
time: totalTime,
|
||||||
|
}
|
||||||
|
const source = {
|
||||||
|
id: sourceId || String(songId),
|
||||||
|
type: 'track',
|
||||||
|
name: sourceName,
|
||||||
|
}
|
||||||
|
|
||||||
|
const metaJson = buildMetaJson(ctx)
|
||||||
|
const cookieStr = buildCookieStr(ctx)
|
||||||
|
|
||||||
|
const ts = Math.floor(Date.now() / 1000)
|
||||||
|
const played = Math.min(playTime, totalTime)
|
||||||
|
|
||||||
|
const plvBody = buildRecords([
|
||||||
|
{ time: ts, action: '_plv', data: buildPlv(ctx, song, source) },
|
||||||
|
])
|
||||||
|
const pldBody = buildRecords([
|
||||||
|
{ time: ts, action: '_pld', data: buildPld(ctx, song, source, played) },
|
||||||
|
])
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1) 上传 PLV
|
||||||
|
const plv = await doUpload(ctx, metaJson, plvBody, cookieStr, 'PLV')
|
||||||
|
if (!plv.success) {
|
||||||
|
const rateMsg =
|
||||||
|
plv.respBody?.data?.rate != null
|
||||||
|
? ` (rate=${plv.respBody.data.rate})`
|
||||||
|
: ''
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
code: plv.respBody?.code || -1,
|
||||||
|
msg: `PLV 上报失败${rateMsg}`,
|
||||||
|
details: plv.respBody,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) 上传 PLD
|
||||||
|
const pld = await doUpload(ctx, metaJson, pldBody, cookieStr, 'PLD')
|
||||||
|
if (!pld.success) {
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
code: pld.respBody?.code || -1,
|
||||||
|
msg: 'PLV 成功但 PLD 失败',
|
||||||
|
details: { plv: plv.respBody, pld: pld.respBody },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
code: 200,
|
||||||
|
data: 'scrobble_v1 上报成功',
|
||||||
|
details: {
|
||||||
|
plv: { fileName: plv.fileName, payloadSize: plv.payload.length },
|
||||||
|
pld: { fileName: pld.fileName, payloadSize: pld.payload.length },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
status: 502,
|
||||||
|
body: { code: 502, msg: `请求异常: ${err.message || err}` },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,5 +7,9 @@ module.exports = (query, request) => {
|
|||||||
msg: query.msg || '',
|
msg: query.msg || '',
|
||||||
id: query.id || '',
|
id: query.id || '',
|
||||||
}
|
}
|
||||||
return request(`/api/share/friends/resource`, data, createOption(query))
|
return request(
|
||||||
|
`/api/share/friends/resource`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'xeapi', 'v3'),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
// 从云盘获取歌曲下载链接
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
songId: query.id,
|
||||||
|
}
|
||||||
|
return request(`/api/cloud/dowonload`, data, createOption(query, 'eapi'))
|
||||||
|
}
|
||||||
@@ -1,13 +1,23 @@
|
|||||||
// 获取客户端歌曲下载链接 - v1
|
// 获取客户端歌曲下载链接 - v1
|
||||||
// 此版本不再采用 br 作为音质区分的标准
|
// 此版本不再采用 br 作为音质区分的标准
|
||||||
// 而是采用 standard, exhigh, lossless, hires, jyeffect(高清环绕声), sky(沉浸环绕声), jymaster(超清母带) 进行音质判断
|
// 而是采用 standard, exhigh, lossless, hires, jyeffect(高清臻音), vivid(臻音全景声), jymaster(超清母带), sky(沉浸环绕声) 进行音质判断
|
||||||
|
|
||||||
const createOption = require('../util/option.js')
|
const createOption = require('../util/option.js')
|
||||||
|
const { cookieToJson } = require('../util/index.js')
|
||||||
module.exports = (query, request) => {
|
module.exports = (query, request) => {
|
||||||
const data = {
|
const data = {
|
||||||
id: query.id,
|
id: query.id,
|
||||||
immerseType: 'c51',
|
immerseType: 'c51',
|
||||||
level: query.level,
|
level: query.level,
|
||||||
}
|
}
|
||||||
return request(`/api/song/enhance/download/url/v1`, data, createOption(query))
|
const options = createOption(query)
|
||||||
|
if (query.level === 'vivid') {
|
||||||
|
const cookie = options.cookie
|
||||||
|
options.cookie = {
|
||||||
|
...(typeof cookie === 'string' ? cookieToJson(cookie) : cookie),
|
||||||
|
os: 'android',
|
||||||
|
appver: '9.5.61',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return request(`/api/song/enhance/download/url/v1`, data, options)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
// 插播相似歌曲
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
positionCode: 'toolBarRcmdSong',
|
||||||
|
resourceId: query.id,
|
||||||
|
resourceType: 'song',
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
`/api/link/position/show/resource`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'eapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
+15
-3
@@ -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,7 +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(`/api/song/enhance/player/url/v1`, data, createOption(query))
|
if (data.level == 'vivid') {
|
||||||
|
data.encodeType = 'mp3'
|
||||||
|
const cookie = options.cookie
|
||||||
|
options.cookie = {
|
||||||
|
...(typeof cookie === 'string' ? cookieToJson(cookie) : cookie),
|
||||||
|
os: 'android',
|
||||||
|
appver: '9.5.61',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return request(`/api/song/enhance/player/url/v1`, data, options)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,30 @@
|
|||||||
// 获取客户端歌曲下载链接 - v1
|
// 获取客户端歌曲下载链接 - v1
|
||||||
// 此版本不再采用 br 作为音质区分的标准
|
// 此版本不再采用 br 作为音质区分的标准
|
||||||
// 而是采用 standard, exhigh, lossless, hires, jyeffect(高清环绕声), sky(沉浸环绕声), jymaster(超清母带) 进行音质判断
|
// 而是采用 standard, exhigh, lossless, hires, jyeffect(高清臻音), vivid(臻音全景声), jymaster(超清母带), sky(沉浸环绕声) 进行音质判断
|
||||||
|
|
||||||
const createOption = require('../util/option.js')
|
const createOption = require('../util/option.js')
|
||||||
|
const { cookieToJson } = require('../util/index.js')
|
||||||
module.exports = async (query, request) => {
|
module.exports = async (query, request) => {
|
||||||
const data = {
|
const data = {
|
||||||
id: query.id,
|
id: query.id,
|
||||||
immerseType: 'c51',
|
immerseType: 'c51',
|
||||||
level: query.level,
|
level: query.level,
|
||||||
}
|
}
|
||||||
|
const options = createOption(query)
|
||||||
|
if (query.level === 'vivid') {
|
||||||
|
const cookie = options.cookie
|
||||||
|
options.cookie = {
|
||||||
|
...(typeof cookie === 'string' ? cookieToJson(cookie) : cookie),
|
||||||
|
os: 'android',
|
||||||
|
appver: '9.5.61',
|
||||||
|
}
|
||||||
|
}
|
||||||
const response = await request(
|
const response = await request(
|
||||||
`/api/song/enhance/download/url/v1`,
|
`/api/song/enhance/download/url/v1`,
|
||||||
data,
|
data,
|
||||||
createOption(query),
|
options,
|
||||||
)
|
)
|
||||||
let url = response?.body?.data?.[0]?.url
|
let url = response?.body?.data?.url || response?.body?.data?.[0]?.url
|
||||||
|
|
||||||
if (!url) {
|
if (!url) {
|
||||||
const fallbackData = {
|
const fallbackData = {
|
||||||
@@ -25,10 +35,13 @@ module.exports = async (query, request) => {
|
|||||||
if (query.level === 'sky') {
|
if (query.level === 'sky') {
|
||||||
fallbackData.immerseType = 'c51'
|
fallbackData.immerseType = 'c51'
|
||||||
}
|
}
|
||||||
|
if (query.level === 'vivid') {
|
||||||
|
fallbackData.encodeType = 'mp3'
|
||||||
|
}
|
||||||
const fallback = await request(
|
const fallback = await request(
|
||||||
`/api/song/enhance/player/url/v1`,
|
`/api/song/enhance/player/url/v1`,
|
||||||
fallbackData,
|
fallbackData,
|
||||||
createOption(query),
|
options,
|
||||||
)
|
)
|
||||||
url = fallback?.body?.data?.[0]?.url
|
url = fallback?.body?.data?.[0]?.url
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
// 歌曲百科
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const extJson = {
|
||||||
|
states: {
|
||||||
|
playingResource: {
|
||||||
|
current: query.id,
|
||||||
|
scene: 'songWiki',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const data = {
|
||||||
|
extJson: JSON.stringify(extJson),
|
||||||
|
positionCode: 'songWikiMainPosition',
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
`/api/link/page/parent/relation/construct/info`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'eapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
// 云小编获取任务
|
||||||
|
//
|
||||||
|
// type:
|
||||||
|
// 1: 歌曲曲风审核 musicalStyleEnter
|
||||||
|
// 2: 歌曲语种审核 languageEnter
|
||||||
|
// 3: 歌曲原唱审核 oriSingerEnter
|
||||||
|
// 4: 情绪标签审核 emotionEnter
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
type: query.type || '4',
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
`/api/thinktank/audit/resource/detail`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'eapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
// 云小编提交任务
|
||||||
|
//
|
||||||
|
// type:
|
||||||
|
// 1: 歌曲曲风审核 musicalStyleEnter
|
||||||
|
// 2: 歌曲语种审核 languageEnter
|
||||||
|
// 3: 歌曲原唱审核 oriSingerEnter
|
||||||
|
// 4: 情绪标签审核 emotionEnter
|
||||||
|
//
|
||||||
|
// taskId:
|
||||||
|
// 调用 thinktank/audit/resource/detail 获取
|
||||||
|
//
|
||||||
|
// judgement:
|
||||||
|
// 1: 同意, 2: 否决, 3: 跳过 (不算次数)
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
if (!query.taskId || !query.judgement)
|
||||||
|
return Promise.reject({
|
||||||
|
status: 400,
|
||||||
|
body: {
|
||||||
|
code: 400,
|
||||||
|
message: '参数不足',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const data = {
|
||||||
|
type: query.type || '4',
|
||||||
|
taskId: query.taskId,
|
||||||
|
judgement: query.judgement,
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
`/api/thinktank/audit/resource/update`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'eapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -4,9 +4,10 @@ const createOption = require('../util/option.js')
|
|||||||
module.exports = (query, request) => {
|
module.exports = (query, request) => {
|
||||||
const data = {
|
const data = {
|
||||||
getcounts: true,
|
getcounts: true,
|
||||||
time: query.lasttime || -1,
|
time: query.lasttime ?? -1,
|
||||||
limit: query.limit || 30,
|
limit: query.limit ?? 30,
|
||||||
total: false,
|
total: false,
|
||||||
|
fromRN: 'true',
|
||||||
}
|
}
|
||||||
return request(`/api/event/get/${query.uid}`, data, createOption(query))
|
return request(`/api/event/get/${query.uid}`, data, createOption(query))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
// 获取当前登录用户可被上游枚举的全部动态
|
||||||
|
|
||||||
|
const userAccount = require('./user_account.js')
|
||||||
|
const userEvent = require('./user_event.js')
|
||||||
|
|
||||||
|
const PAGE_SIZE = 100
|
||||||
|
const MAX_PAGES = 1000
|
||||||
|
|
||||||
|
const errorResponse = (message, cookie = []) => ({
|
||||||
|
status: 502,
|
||||||
|
body: {
|
||||||
|
code: 502,
|
||||||
|
message,
|
||||||
|
},
|
||||||
|
cookie,
|
||||||
|
})
|
||||||
|
|
||||||
|
module.exports = async (query, request) => {
|
||||||
|
const accountResult = await userAccount(query, request)
|
||||||
|
const uid =
|
||||||
|
accountResult.body?.account?.id || accountResult.body?.profile?.userId
|
||||||
|
|
||||||
|
if (!uid) {
|
||||||
|
if (
|
||||||
|
accountResult.status !== 200 ||
|
||||||
|
(accountResult.body?.code && accountResult.body.code !== 200)
|
||||||
|
) {
|
||||||
|
return accountResult
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: 401,
|
||||||
|
body: {
|
||||||
|
code: 401,
|
||||||
|
message: 'A valid login cookie is required',
|
||||||
|
},
|
||||||
|
cookie: accountResult.cookie || [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cookies = [...(accountResult.cookie || [])]
|
||||||
|
const events = []
|
||||||
|
const eventIds = new Set()
|
||||||
|
const cursors = new Set()
|
||||||
|
let lasttime = -1
|
||||||
|
let more = true
|
||||||
|
let pageCount = 0
|
||||||
|
let size = null
|
||||||
|
|
||||||
|
while (more) {
|
||||||
|
pageCount += 1
|
||||||
|
|
||||||
|
const pageResult = await userEvent(
|
||||||
|
{
|
||||||
|
...query,
|
||||||
|
uid,
|
||||||
|
lasttime,
|
||||||
|
limit: PAGE_SIZE,
|
||||||
|
},
|
||||||
|
request,
|
||||||
|
)
|
||||||
|
|
||||||
|
cookies.push(...(pageResult.cookie || []))
|
||||||
|
|
||||||
|
if (pageResult.status !== 200 || pageResult.body?.code !== 200) {
|
||||||
|
return {
|
||||||
|
...pageResult,
|
||||||
|
cookie: cookies,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pageCount === 1) {
|
||||||
|
const reportedSize = pageResult.body.size
|
||||||
|
const numericSize = Number(reportedSize)
|
||||||
|
size =
|
||||||
|
reportedSize != null && Number.isFinite(numericSize)
|
||||||
|
? numericSize
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const event of pageResult.body.events || []) {
|
||||||
|
if (event?.id == null) {
|
||||||
|
events.push(event)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventId = String(event.id)
|
||||||
|
if (!eventIds.has(eventId)) {
|
||||||
|
eventIds.add(eventId)
|
||||||
|
events.push(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
more = Boolean(pageResult.body.more)
|
||||||
|
lasttime = pageResult.body.lasttime
|
||||||
|
|
||||||
|
if (more) {
|
||||||
|
const cursor = String(lasttime ?? '')
|
||||||
|
if (!cursor || cursors.has(cursor)) {
|
||||||
|
return errorResponse(
|
||||||
|
'Upstream event pagination cursor stalled',
|
||||||
|
cookies,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
cursors.add(cursor)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (more && pageCount >= MAX_PAGES) {
|
||||||
|
return errorResponse(
|
||||||
|
`Upstream event pagination exceeded ${MAX_PAGES} pages`,
|
||||||
|
cookies,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const retrievedCount = events.length
|
||||||
|
const unavailableCount =
|
||||||
|
size == null ? null : Math.max(size - retrievedCount, 0)
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
code: 200,
|
||||||
|
events,
|
||||||
|
size,
|
||||||
|
retrievedCount,
|
||||||
|
unavailableCount,
|
||||||
|
sizeMismatch: size == null ? null : size !== retrievedCount,
|
||||||
|
pageCount,
|
||||||
|
more: false,
|
||||||
|
lasttime: lasttime ?? null,
|
||||||
|
},
|
||||||
|
cookie: cookies,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// 一键领取所有会员成长值
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {}
|
||||||
|
return request(
|
||||||
|
`/api/vipnewcenter/app/level/task/reward/getall`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'xeapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
+36
-3
@@ -1,7 +1,40 @@
|
|||||||
// 黑胶乐签打卡
|
// 黑胶乐签打卡
|
||||||
|
|
||||||
const createOption = require('../util/option.js')
|
const createOption = require('../util/option.js')
|
||||||
module.exports = (query, request) => {
|
|
||||||
const data = {}
|
module.exports = async (query, request) => {
|
||||||
return request(`/api/vip-center-bff/task/sign`, data, createOption(query))
|
const results = {}
|
||||||
|
|
||||||
|
const taskSign = await request(
|
||||||
|
'/api/vip-center-bff/task/sign',
|
||||||
|
{},
|
||||||
|
createOption(query, 'weapi'),
|
||||||
|
)
|
||||||
|
results.taskSign = taskSign.body
|
||||||
|
|
||||||
|
const checkinDetail = await request(
|
||||||
|
'/api/vipnewcenter/app/level/user/checkin/history/detail',
|
||||||
|
{
|
||||||
|
signDayTime: Date.now(),
|
||||||
|
type: 1,
|
||||||
|
},
|
||||||
|
createOption(query, 'eapi'),
|
||||||
|
)
|
||||||
|
results.checkinDetail = checkinDetail.body
|
||||||
|
|
||||||
|
// 两个接口都返回 code=200 即视为打卡成功
|
||||||
|
const signed =
|
||||||
|
Number(results.taskSign?.code) === 200 &&
|
||||||
|
Number(results.checkinDetail?.code) === 200
|
||||||
|
|
||||||
|
return {
|
||||||
|
body: {
|
||||||
|
code: 200,
|
||||||
|
...results,
|
||||||
|
signed,
|
||||||
|
message: signed ? '黑胶乐签打卡成功' : '黑胶乐签打卡失败',
|
||||||
|
},
|
||||||
|
cookie: taskSign.cookie,
|
||||||
|
status: 200,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
// 黑胶乐签打卡详情
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
signDayTime: query.timestamp,
|
||||||
|
type: '1',
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
`/api/vipnewcenter/app/level/user/checkin/history/detail`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'eapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
// 黑胶乐签打卡历史 / 状态查询
|
||||||
|
// 支持传入 type=0(用户信息栏)或 type=1(黑胶乐签)
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
type: query.type || '0',
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
`/api/vipnewcenter/app/minidesk/music/sign/pc`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'eapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
// 黑胶乐签签到信息
|
// 黑胶乐签未来签到信息
|
||||||
|
|
||||||
const createOption = require('../util/option.js')
|
const createOption = require('../util/option.js')
|
||||||
module.exports = (query, request) => {
|
module.exports = (query, request) => {
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
// 会员任务 - 新版
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
taskType: 'app_vip_task_center',
|
||||||
|
userId: query.id,
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
`/api/middle/vip/mission/user/progress/list`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'xeapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
+38
-50
@@ -2,6 +2,7 @@ const { default: axios } = require('axios')
|
|||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
var xml2js = require('xml2js')
|
var xml2js = require('xml2js')
|
||||||
|
|
||||||
|
const uploadPlugin = require('../plugins/upload')
|
||||||
const createOption = require('../util/option.js')
|
const createOption = require('../util/option.js')
|
||||||
const { getFileExtension, readFileChunk } = require('../util/fileHelper')
|
const { getFileExtension, readFileChunk } = require('../util/fileHelper')
|
||||||
|
|
||||||
@@ -19,15 +20,7 @@ function createDupkey() {
|
|||||||
return s.join('')
|
return s.join('')
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = async (query, request) => {
|
module.exports = async (query, request, dependencies = {}) => {
|
||||||
const ext = getFileExtension(query.songFile.name)
|
|
||||||
const filename =
|
|
||||||
query.songName ||
|
|
||||||
query.songFile.name
|
|
||||||
.replace('.' + ext, '')
|
|
||||||
.replace(/\s/g, '')
|
|
||||||
.replace(/\./g, '_')
|
|
||||||
|
|
||||||
if (!query.songFile) {
|
if (!query.songFile) {
|
||||||
return Promise.reject({
|
return Promise.reject({
|
||||||
status: 500,
|
status: 500,
|
||||||
@@ -38,6 +31,19 @@ module.exports = async (query, request) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const axiosRequest = dependencies.axios || axios
|
||||||
|
const uploadImage = dependencies.uploadPlugin || uploadPlugin
|
||||||
|
const ext = getFileExtension(query.songFile.name)
|
||||||
|
const filename =
|
||||||
|
query.songName ||
|
||||||
|
query.songFile.name
|
||||||
|
.replace('.' + ext, '')
|
||||||
|
.replace(/\s/g, '')
|
||||||
|
.replace(/\./g, '_')
|
||||||
|
const coverImgId = query.imgFile
|
||||||
|
? (await uploadImage(query, request)).imgId
|
||||||
|
: query.coverImgId
|
||||||
|
|
||||||
const tokenRes = await request(
|
const tokenRes = await request(
|
||||||
`/api/nos/token/alloc`,
|
`/api/nos/token/alloc`,
|
||||||
{
|
{
|
||||||
@@ -53,7 +59,7 @@ module.exports = async (query, request) => {
|
|||||||
|
|
||||||
const objectKey = tokenRes.body.result.objectKey.replace(/\//g, '%2F')
|
const objectKey = tokenRes.body.result.objectKey.replace(/\//g, '%2F')
|
||||||
const docId = tokenRes.body.result.docId
|
const docId = tokenRes.body.result.docId
|
||||||
const res = await axios({
|
const res = await axiosRequest({
|
||||||
method: 'post',
|
method: 'post',
|
||||||
url: `https://ymusic.nos-hz.163yun.com/${objectKey}?uploads`,
|
url: `https://ymusic.nos-hz.163yun.com/${objectKey}?uploads`,
|
||||||
headers: {
|
headers: {
|
||||||
@@ -94,7 +100,7 @@ module.exports = async (query, request) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const res3 = await axios({
|
const res3 = await axiosRequest({
|
||||||
method: 'put',
|
method: 'put',
|
||||||
url: `https://ymusic.nos-hz.163yun.com/${objectKey}?partNumber=${blockIndex}&uploadId=${res2.InitiateMultipartUploadResult.UploadId[0]}`,
|
url: `https://ymusic.nos-hz.163yun.com/${objectKey}?partNumber=${blockIndex}&uploadId=${res2.InitiateMultipartUploadResult.UploadId[0]}`,
|
||||||
headers: {
|
headers: {
|
||||||
@@ -117,7 +123,7 @@ module.exports = async (query, request) => {
|
|||||||
}
|
}
|
||||||
completeStr += '</CompleteMultipartUpload>'
|
completeStr += '</CompleteMultipartUpload>'
|
||||||
|
|
||||||
await axios({
|
await axiosRequest({
|
||||||
method: 'post',
|
method: 'post',
|
||||||
url: `https://ymusic.nos-hz.163yun.com/${objectKey}?uploadId=${res2.InitiateMultipartUploadResult.UploadId[0]}`,
|
url: `https://ymusic.nos-hz.163yun.com/${objectKey}?uploadId=${res2.InitiateMultipartUploadResult.UploadId[0]}`,
|
||||||
headers: {
|
headers: {
|
||||||
@@ -128,29 +134,29 @@ module.exports = async (query, request) => {
|
|||||||
data: completeStr,
|
data: completeStr,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const voiceData = JSON.stringify([
|
||||||
|
{
|
||||||
|
name: filename,
|
||||||
|
autoPublish: query.autoPublish == 1 ? true : false,
|
||||||
|
autoPublishText: query.autoPublishText || '',
|
||||||
|
description: query.description,
|
||||||
|
voiceListId: query.voiceListId,
|
||||||
|
coverImgId,
|
||||||
|
dfsId: docId,
|
||||||
|
categoryId: query.categoryId,
|
||||||
|
secondCategoryId: query.secondCategoryId,
|
||||||
|
composedSongs: query.composedSongs ? query.composedSongs.split(',') : [],
|
||||||
|
privacy: query.privacy == 1 ? true : false,
|
||||||
|
publishTime: query.publishTime || 0,
|
||||||
|
orderNo: query.orderNo || 1,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
await request(
|
await request(
|
||||||
`/api/voice/workbench/voice/batch/upload/preCheck`,
|
`/api/voice/workbench/voice/batch/upload/preCheck`,
|
||||||
{
|
{
|
||||||
dupkey: createDupkey(),
|
dupkey: createDupkey(),
|
||||||
voiceData: JSON.stringify([
|
voiceData,
|
||||||
{
|
|
||||||
name: filename,
|
|
||||||
autoPublish: query.autoPublish == 1 ? true : false,
|
|
||||||
autoPublishText: query.autoPublishText || '',
|
|
||||||
description: query.description,
|
|
||||||
voiceListId: query.voiceListId,
|
|
||||||
coverImgId: query.coverImgId,
|
|
||||||
dfsId: docId,
|
|
||||||
categoryId: query.categoryId,
|
|
||||||
secondCategoryId: query.secondCategoryId,
|
|
||||||
composedSongs: query.composedSongs
|
|
||||||
? query.composedSongs.split(',')
|
|
||||||
: [],
|
|
||||||
privacy: query.privacy == 1 ? true : false,
|
|
||||||
publishTime: query.publishTime || 0,
|
|
||||||
orderNo: query.orderNo || 1,
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
...createOption(query),
|
...createOption(query),
|
||||||
@@ -163,25 +169,7 @@ module.exports = async (query, request) => {
|
|||||||
`/api/voice/workbench/voice/batch/upload/v2`,
|
`/api/voice/workbench/voice/batch/upload/v2`,
|
||||||
{
|
{
|
||||||
dupkey: createDupkey(),
|
dupkey: createDupkey(),
|
||||||
voiceData: JSON.stringify([
|
voiceData,
|
||||||
{
|
|
||||||
name: filename,
|
|
||||||
autoPublish: query.autoPublish == 1 ? true : false,
|
|
||||||
autoPublishText: query.autoPublishText || '',
|
|
||||||
description: query.description,
|
|
||||||
voiceListId: query.voiceListId,
|
|
||||||
coverImgId: query.coverImgId,
|
|
||||||
dfsId: docId,
|
|
||||||
categoryId: query.categoryId,
|
|
||||||
secondCategoryId: query.secondCategoryId,
|
|
||||||
composedSongs: query.composedSongs
|
|
||||||
? query.composedSongs.split(',')
|
|
||||||
: [],
|
|
||||||
privacy: query.privacy == 1 ? true : false,
|
|
||||||
publishTime: query.publishTime || 0,
|
|
||||||
orderNo: query.orderNo || 1,
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
...createOption(query),
|
...createOption(query),
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
const createOption = require('../util/option.js')
|
const createOption = require('../util/option.js')
|
||||||
module.exports = (query, request) => {
|
module.exports = (query, request) => {
|
||||||
const data = {}
|
const data = {}
|
||||||
return request(`/api/pointmall/user/sign`, data, createOption(query, 'weapi'))
|
return request(
|
||||||
|
`/api/pointmall/user/sign`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'xeapi', 'v3'),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
// 云贝广告任务 - 完成任务领取云贝
|
||||||
|
// 逆向来源: 云贝任务中心 H5 (st.music.163.com/yunbei-listen) main.js
|
||||||
|
// POST /api/ad/power/yunbei/distribution/create
|
||||||
|
// 参数: yunbeiAmount (单次可得云贝, 客户端从 list 接口的 singleAmount 取值, 当前为 150)
|
||||||
|
// 返回: true (领取成功)
|
||||||
|
//
|
||||||
|
// 实测验证 (2026-08-05):
|
||||||
|
// - 仅传 yunbeiAmount 即可成功领取, 无需真实听歌/看视频
|
||||||
|
// - 单日上限 10 次 × 150 云贝 = 1500 云贝/天
|
||||||
|
// - 超限返回 code:400 "单日完成任务数已达上限"
|
||||||
|
// - 无频率限制, 800ms 间隔连续调用均成功
|
||||||
|
//
|
||||||
|
// 建议: 领取前先调 yunbei_ad_task_list 查询 times, 达到 10 次即停止
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
yunbeiAmount: query.yunbeiAmount || 150,
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
`/api/ad/power/yunbei/distribution/create`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'weapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
// 云贝广告任务 - 查询今日任务状态
|
||||||
|
// 逆向来源: 云贝任务中心 H5 (st.music.163.com/yunbei-listen) main.js
|
||||||
|
// GET /api/ad/power/yunbei/distribution/list
|
||||||
|
// 返回: { times: 今日已完成次数, amount: 今日累计云贝, singleAmount: 单次可得云贝 }
|
||||||
|
// 注意: 单日上限 10 次, 单次 150 云贝 (每天最多 1500)
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {}
|
||||||
|
return request(
|
||||||
|
`/api/ad/power/yunbei/distribution/list`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'weapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
// 云贝广告任务 - 获取推荐歌曲
|
||||||
|
// 逆向来源: 云贝任务中心 H5 (st.music.163.com/yunbei-listen) main.js
|
||||||
|
// POST /api/ad/power/yunbei/distribution/recommend/song
|
||||||
|
// 参数: offset (默认 0), limit (默认 10, 客户端一次 10 首)
|
||||||
|
// 返回: 推荐歌曲数组 [{ songId, songName, artistName, albumUrl, songChorusStartTime, likeFlag, alg }]
|
||||||
|
// 注意: alg 均为 alg_payrec_yunBei_*, 为"听歌得云贝"任务专属推荐
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
offset: query.offset || 0,
|
||||||
|
limit: query.limit || 10,
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
`/api/ad/power/yunbei/distribution/recommend/song`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'weapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
import { banner, lyric } from '@neteasecloudmusicapienhanced/api'
|
import { banner, lyric } from '@neteasecloudmusicapienhanced/api'
|
||||||
import { logger } from '../util/logger.js'
|
import type { Response } from '@neteasecloudmusicapienhanced/api'
|
||||||
banner({ type: 0 }).then((res) => {
|
import logger from '../util/logger.js'
|
||||||
|
banner({ type: 0 }).then((res: Response) => {
|
||||||
logger.info(res)
|
logger.info(res)
|
||||||
})
|
})
|
||||||
lyric({
|
lyric({
|
||||||
id: '33894312',
|
id: '33894312',
|
||||||
}).then((res) => {
|
}).then((res: Response) => {
|
||||||
logger.info(res)
|
logger.info(res)
|
||||||
})
|
})
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user