mirror of
https://github.com/NeteaseCloudMusicApiEnhanced/api-enhanced.git
synced 2026-10-20 22:51:23 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c3e0258b85 | ||
|
|
843c19f089 | ||
|
|
c50aeddf5f | ||
|
|
b8af386082 | ||
|
|
d10296ccd8 | ||
|
|
805d0bccc5 | ||
|
|
5fc9346e0e | ||
|
|
e79490327d | ||
|
|
a30df5ae7f | ||
|
|
06e4722225 | ||
|
|
e0bcb214cb | ||
|
|
5478041ddd | ||
|
|
5105a9a09d | ||
|
|
e3a43a807a | ||
|
|
18715ca75d | ||
|
|
e72f0dfbff | ||
|
|
54bdcf9482 | ||
|
|
c4ad7a25c5 | ||
|
|
03eb677b27 | ||
|
|
a22c798a8d | ||
|
|
712c26d3da | ||
|
|
b725db1380 | ||
|
|
0de5526d17 | ||
|
|
78fef459c8 | ||
|
|
090eb4e085 | ||
|
|
619fd10f4b | ||
|
|
e219072f3c | ||
|
|
a345848533 | ||
|
|
4e1307166a | ||
|
|
8f9d38529c |
@@ -1,5 +0,0 @@
|
||||
# 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
|
||||
+1
-2
@@ -7,9 +7,8 @@
|
||||
!/app.js
|
||||
!/server.js
|
||||
!/package.json
|
||||
!/package-lock.json
|
||||
!/index.js
|
||||
!/generateConfig.js
|
||||
!/main.js
|
||||
!/data
|
||||
!/.env
|
||||
!/pnpm-lock.yaml
|
||||
@@ -1,27 +0,0 @@
|
||||
### CORS资源共享设置
|
||||
## 如果你需要配置, 请取消下面的注释并设置具体的域名
|
||||
# CORS_ALLOW_ORIGIN = "*"
|
||||
|
||||
### 代理设置
|
||||
## 启用反向代理
|
||||
ENABLE_PROXY = false
|
||||
## 代理配置
|
||||
PROXY_URL = "https://your-proxy-url.com/?proxy="
|
||||
|
||||
### 随机IP设置
|
||||
## 启用随机中国IP(默认关闭);启用后,所有请求默认使用随机中国IP,除非请求参数randomCNIP显式关闭
|
||||
ENABLE_RANDOM_CN_IP = false
|
||||
|
||||
### UnblockNeteaseMusic 设置项
|
||||
## 启用全局解灰, 无论是否调用参数都会使用解灰(不推荐开启)
|
||||
ENABLE_GENERAL_UNBLOCK = false
|
||||
## 歌曲启用无损音质
|
||||
ENABLE_FLAC = true
|
||||
## 启用无损音质时,是否选择音质最高的
|
||||
SELECT_MAX_BR = false
|
||||
## 严格按照配置音源设置顺序进行匹配
|
||||
FOLLOW_SOURCE_ORDER = true
|
||||
## Cookie设置项; 推荐在Vercel等平台进行环境变量的设置或者设置github secret
|
||||
# 网易云 cookie
|
||||
# 格式:MUSIC_U=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
# NETEASE_COOKIE = ""
|
||||
@@ -0,0 +1,57 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
|
||||
ignorePatterns: ['public/'],
|
||||
parserOptions: {
|
||||
parser: 'babel-eslint',
|
||||
ecmaVersion: 2018,
|
||||
sourceType: 'module',
|
||||
},
|
||||
plugins: ['html'],
|
||||
extends: ['plugin:prettier/recommended'],
|
||||
env: {
|
||||
browser: true,
|
||||
node: true,
|
||||
},
|
||||
|
||||
rules: {
|
||||
'prettier/prettier': [
|
||||
'error',
|
||||
{
|
||||
endOfLine: 'auto',
|
||||
},
|
||||
],
|
||||
indent: ['error', 2, { SwitchCase: 1 }],
|
||||
'space-infix-ops': ['error', { int32Hint: false }],
|
||||
'key-spacing': [
|
||||
2,
|
||||
{
|
||||
beforeColon: false,
|
||||
afterColon: true,
|
||||
},
|
||||
],
|
||||
'no-octal': 2,
|
||||
'no-redeclare': 2,
|
||||
'comma-spacing': 2,
|
||||
'no-new-object': 2,
|
||||
'arrow-spacing': 2,
|
||||
quotes: [
|
||||
2,
|
||||
'single',
|
||||
{
|
||||
avoidEscape: true,
|
||||
allowTemplateLiterals: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ['**/*.ts'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
extends: [
|
||||
'plugin:@typescript-eslint/recommended', // Uses the recommended rules from the @typescript-eslint/eslint-plugin
|
||||
// 'prettier/@typescript-eslint',
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
custom: ["https://rewards.focalors.ltd"]
|
||||
@@ -0,0 +1,42 @@
|
||||
---
|
||||
name: 创建问题反馈
|
||||
about: 问题反馈
|
||||
title: "<不按照模版格式或者删除模版信息将得不到处理,确认看完了调用前须知再发 issues>"
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
## 环境
|
||||
- 系统/平台: <你的系统和平台>
|
||||
|
||||
- nodejs 版本: <你的 NodeJS 版本号>
|
||||
|
||||
- API版本: <运行的云音乐 API 的版本号, 对应 package.json 里面的 version>
|
||||
|
||||
## 出现问题
|
||||
<请清晰描述出现的问题,不要使用xxx不行之类的模糊不清的语句,我没有读心术>
|
||||
|
||||
## 重现步骤
|
||||
<请清晰描述重现步骤,调用的接口链接和参数以及命令行错误信息,最好贴上截图(命令行错误信息和接口返回内容)>
|
||||
|
||||
## 期待效果
|
||||
<请清晰描述现在的效果和期待的效果>
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
>维护项目都是业余时间,精力有限,我只能挑容易解决的issues处理,为了节约双方时间,请尽可能提供足够的有用的信息,给的信息不够我只能根据精力和时间看情况处理,如果模板信息看都不看就删掉,我不会进行任何回复,并且一个月后close掉issue
|
||||
|
||||
>先看文档有没有相关说明,调用前须知必看,确认看完了调用前须知再发 issues
|
||||
|
||||
>先在 issues 搜一下是否有相似问题,没有再发,否则直接关闭
|
||||
|
||||
>不处理别人搭建的线上服务的问题,此项目不提供任何线上服务,请自行搭建
|
||||
|
||||
>重现步骤尽量详细,不能含糊不清,包含请求地址和对应参数以及操作过程描述,不是每个人都喜欢猜别人遇到了什么问题和找参数一个个试,也比较浪费时间
|
||||
|
||||
>如果不是提建议,提 issues 如果不照着模版来将不会优先处理或放着不管
|
||||
|
||||
>460 cheating 的问题把 `utils/request.js` 里面的 `headers['X-Real-IP']` 的注释取消掉就好
|
||||
@@ -0,0 +1,11 @@
|
||||
---
|
||||
name: 功能需求
|
||||
about: 希望添加新接口
|
||||
title: ''
|
||||
labels: 功能需求
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**功能描述**
|
||||
- 请清晰描述你所希望添加的功能,最好贴上对应客户端界面的截图
|
||||
@@ -1,117 +0,0 @@
|
||||
name: 🐛 Bug报告
|
||||
description: 报告API其中的错误或问题
|
||||
title: "[Bug] "
|
||||
labels: ["bug"]
|
||||
assignees: []
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
感谢您报告Bug!请填写以下信息帮助我们快速定位和修复问题。
|
||||
|
||||
- type: checkboxes
|
||||
id: terms
|
||||
attributes:
|
||||
label: 确认事项
|
||||
description: |
|
||||
在提交Bug报告前,请确认以下事项:
|
||||
|
||||
- 我已经搜索了现有的issues,确认这不是重复问题
|
||||
- 我使用的是最新版本的API, 而且是官方发布的版本,而不是fork或修改版
|
||||
- 不处理别人搭建的线上服务的问题,此项目提供任何线上服务不保证质量
|
||||
- 如果不是提建议,提 issues 如果不照着模版来将不会优先处理或放着不管
|
||||
- 维护项目都是业余时间,精力有限,我只能挑容易解决的issues处理,为了节约双方时间,请尽可能提供足够的有用的信息,给的信息不够我只能根据精力和时间看情况处理,如果模板信息看都不看就删掉,我不会进行任何回复,并且一个月后close掉issue
|
||||
|
||||
options:
|
||||
- label: 我已确认以上事项
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: API版本
|
||||
description: 请提供您使用的API版本以及nodejs版本
|
||||
placeholder: "v4.2x.x, Node.js v22.x.x"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: deployment
|
||||
attributes:
|
||||
label: 部署方式
|
||||
description: 此项目支持本地部署和云平台部署,您使用的是哪种方式?
|
||||
options:
|
||||
- 本机直接运行 (node / pm2)
|
||||
- 本机 Docker 部署
|
||||
- Vercel
|
||||
- Heroku
|
||||
- Railway
|
||||
- 其他云平台 (请在描述中说明)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: bug-description
|
||||
attributes:
|
||||
label: Bug描述
|
||||
description: 请详细描述您遇到的问题
|
||||
placeholder: |
|
||||
清晰简洁地描述bug的具体表现...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: reproduction-steps
|
||||
attributes:
|
||||
label: 重现步骤
|
||||
description: 请提供详细的重现步骤
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: expected-behavior
|
||||
attributes:
|
||||
label: 期望行为
|
||||
description: 请描述您期望发生的行为
|
||||
placeholder: 清晰简洁地描述您期望应该发生什么...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: actual-behavior
|
||||
attributes:
|
||||
label: 实际行为
|
||||
description: 请描述实际发生的行为
|
||||
placeholder: 清晰简洁地描述实际发生了什么...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: error-output
|
||||
attributes:
|
||||
label: 错误输出日志
|
||||
description: 请提供完整的错误信息日志
|
||||
render: shell
|
||||
placeholder: |
|
||||
错误信息输出...
|
||||
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: 额外信息
|
||||
description: 添加任何其他有助于理解问题的信息
|
||||
placeholder: |
|
||||
- 截图
|
||||
- 相关配置文件
|
||||
- 其他相关信息
|
||||
|
||||
- type: checkboxes
|
||||
id: help
|
||||
attributes:
|
||||
label: 愿意协助
|
||||
description: 您是否愿意帮助修复这个bug?
|
||||
options:
|
||||
- label: 我愿意提交PR来修复这个bug
|
||||
- label: 我可以提供更多测试用例
|
||||
- label: 我可以帮助测试修复方案
|
||||
@@ -1,8 +0,0 @@
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: ↑请尽量使用议题模板创建议题↑
|
||||
url: https://github.com/NeteaseCloudMusicApiEnhanced/api-enhanced/issues/new/choose
|
||||
about: 选择适合的议题模板可以帮助我们更快地定位和解决问题
|
||||
- name: 提问的艺术
|
||||
url: https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/main/README-zh_CN.md
|
||||
about: 默认所有 Issues 发起者均已了解此处的内容
|
||||
@@ -1,73 +0,0 @@
|
||||
name: ✨ 接口添加请求
|
||||
description: 希望添加新接口
|
||||
title: "[Feature] "
|
||||
labels: ["enhancement"]
|
||||
assignees: []
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
感谢您提出新功能建议!请填写以下信息帮助我们理解您的需求。
|
||||
|
||||
- type: checkboxes
|
||||
id: terms
|
||||
attributes:
|
||||
label: 确认事项
|
||||
description: |
|
||||
在提交功能请求前,请确认以下事项
|
||||
|
||||
- 我已经搜索了现有的issues,确认这不是重复请求
|
||||
- 我已经查看了项目文档
|
||||
- 该项目符合该项目的目标和范围
|
||||
- 我确认我提供的接口请求是合理且有意义的
|
||||
|
||||
options:
|
||||
- label: 我已确认以上事项
|
||||
required: true
|
||||
|
||||
|
||||
- type: textarea
|
||||
id: proposed-solution
|
||||
attributes:
|
||||
label: 功能描述
|
||||
description: 请详细描述您希望添加的功能,最好贴上对应客户端界面的截图
|
||||
placeholder: |
|
||||
请详细描述您期望的功能:
|
||||
- 新的网易云音乐api 接口?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: priority
|
||||
attributes:
|
||||
label: 优先级
|
||||
description: 您认为这个功能的优先级如何?
|
||||
options:
|
||||
- 低 (可有可无的改进)
|
||||
- 中 (会显著改善使用体验)
|
||||
- 高 (严重影响了我使用)
|
||||
- 紧急 (阻塞了重要工作)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: compatibility
|
||||
attributes:
|
||||
label: 兼容性考虑
|
||||
description: 这个功能是否会影响现有代码的兼容性?
|
||||
placeholder: |
|
||||
- 是否是破坏性变更?
|
||||
- 是否需要语法变更?
|
||||
- 对性能的影响?
|
||||
|
||||
- type: checkboxes
|
||||
id: implementation
|
||||
attributes:
|
||||
label: 实现意愿
|
||||
description: 您是否愿意参与这个功能的实现?
|
||||
options:
|
||||
- label: 我愿意提交PR来实现这个功能
|
||||
- label: 我可以协助设计API接口
|
||||
- label: 我可以提供测试用例
|
||||
- label: 我可以帮助编写文档
|
||||
@@ -1,41 +0,0 @@
|
||||
# .github/dependabot.yml
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
# 更新 npm (JavaScript) 依赖
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/" # package.json 所在的位置
|
||||
schedule:
|
||||
interval: "weekly" # 每周一检查
|
||||
# 默认情况下,Dependabot 会自动获取最新版本。
|
||||
# 你可以使用 `ignore` 来排除某些特定更新
|
||||
ignore:
|
||||
- dependency-name: "eslint"
|
||||
# 对于 ESLint,忽略主版本 8 以上的所有更新
|
||||
versions: ["> 8"]
|
||||
# 自动合并小版本更新的 PR (可选)
|
||||
# 可以设置针对 patch 或 minor 版本更新自动合并
|
||||
commit-message:
|
||||
prefix: "build(packages):" # PR 合并提交信息的前缀
|
||||
|
||||
# 更新 GitHub Actions 依赖
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/" # .github/workflows/ 目录所在的位置
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
|
||||
# 更新 Python (pip) 依赖,如果你的项目有 requirements.txt
|
||||
- package-ecosystem: "pip"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
|
||||
# 如果你有多个目录(例如一个 monorepo),可以为每个目录单独配置
|
||||
# - package-ecosystem: "npm"
|
||||
# directory: "/packages/frontend"
|
||||
# schedule:
|
||||
# interval: "weekly"
|
||||
# - package-ecosystem: "npm"
|
||||
# directory: "/packages/backend"
|
||||
# schedule:
|
||||
# interval: "weekly"
|
||||
@@ -1,54 +0,0 @@
|
||||
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
|
||||
@@ -1,67 +0,0 @@
|
||||
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
|
||||
@@ -1,54 +0,0 @@
|
||||
name: Issue Management
|
||||
permissions:
|
||||
issues: write
|
||||
contents: read
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened, labeled]
|
||||
|
||||
jobs:
|
||||
welcome-new-issues:
|
||||
if: github.event.action == 'opened'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Welcome new issue
|
||||
uses: peter-evans/create-or-update-comment@v5
|
||||
with:
|
||||
issue-number: ${{ github.event.issue.number }}
|
||||
body: |
|
||||
感谢您在该仓库提出问题!我们非常重视您的反馈,并希望尽快为您提供帮助。
|
||||
|
||||
如果您在一周左右的时间里没有收到我们的回复,请在下方留言,以便我们注意到。我们的大部分审核都是志愿者,有时难免会有疏漏。
|
||||
|
||||
你可能会想要尝试以下方法来获得更快的帮助:
|
||||
- 📖 查看[项目文档](https://neteasecloudmusicapienhanced.js.org)
|
||||
- 💬 加入[QQ交流群](https://qm.qq.com/q/TpeP9Uv2yk)
|
||||
- 🔍 搜索[现有issues](https://github.com/neteasecloudmusicapienhanced/api-enhanced/issues) 看是否有类似问题
|
||||
|
||||
stale-issues:
|
||||
if: github.event.action == 'opened'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Mark stale issues
|
||||
uses: actions/stale@v11
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
stale-issue-message: |
|
||||
这个issue已经30天没有活动了。
|
||||
|
||||
如果这个问题仍然存在,请回复此评论说明当前状态。
|
||||
否则,这个issue将在7天后自动关闭。
|
||||
|
||||
您可以随时重新打开已关闭的issue。
|
||||
stale-pr-message: |
|
||||
这个PR已经30天没有活动了。
|
||||
|
||||
如果您仍在处理这个PR,请回复此评论。
|
||||
否则,这个PR将在7天后自动关闭。
|
||||
days-before-stale: 30
|
||||
days-before-close: 7
|
||||
stale-issue-label: "stale"
|
||||
stale-pr-label: "stale"
|
||||
exempt-issue-labels: "pinned,enhancement,priority:high,ongoing,fatal"
|
||||
exempt-pr-labels: "pinned,priority:high,ongoing,fatal"
|
||||
@@ -1,71 +0,0 @@
|
||||
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 }}
|
||||
@@ -1,315 +0,0 @@
|
||||
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
|
||||
@@ -1,36 +0,0 @@
|
||||
name: Upstream Sync
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 4 * * *" # At 12PM UTC+8
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
sync_latest_from_upstream:
|
||||
name: Sync latest commits from upstream repo
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.repository.fork }}
|
||||
|
||||
steps:
|
||||
# Step 1: run a standard checkout action
|
||||
- name: Checkout target repo
|
||||
uses: actions/checkout@v6
|
||||
|
||||
# Step 2: run the sync action
|
||||
- name: Sync upstream changes
|
||||
id: sync
|
||||
uses: aormsby/Fork-Sync-With-Upstream-action@v3.4.3
|
||||
with:
|
||||
upstream_sync_repo: NeteaseCloudMusicApiEnhanced/api-enhanced
|
||||
upstream_sync_branch: main
|
||||
target_sync_branch: main
|
||||
target_repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Sync check
|
||||
if: failure()
|
||||
run: |
|
||||
echo "[Error] Due to a change in the workflow file of the upstream repository, GitHub has automatically suspended the scheduled automatic update. You need to manually sync your fork."
|
||||
exit 1
|
||||
@@ -8,6 +8,3 @@ examples/moddef.json
|
||||
bin
|
||||
anonymous_token
|
||||
.vercel
|
||||
.env
|
||||
IFLOW.md
|
||||
precompiled
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
language: node_js
|
||||
|
||||
node_js:
|
||||
- 22
|
||||
- 12
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
# 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` 白名单;改依赖安装行为时先看它。
|
||||
+1390
File diff suppressed because it is too large
Load Diff
+3
-8
@@ -2,20 +2,15 @@ FROM node:lts-alpine
|
||||
|
||||
RUN apk add --no-cache tini
|
||||
|
||||
ENV NODE_ENV=production
|
||||
|
||||
RUN npm install -g pnpm@9
|
||||
|
||||
ENV NODE_ENV production
|
||||
USER node
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --chown=node:node . ./
|
||||
|
||||
# --prod 模式下 husky 不会被安装,prepare 脚本会因找不到 husky 而失败,
|
||||
# 故显式跳过生命周期脚本;本项目生产运行也不依赖任何 postinstall 步骤。
|
||||
RUN pnpm install --frozen-lockfile --prod --ignore-scripts
|
||||
RUN yarn --network-timeout=100000
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD [ "/sbin/tini", "--", "node", "app.js" ]
|
||||
CMD [ "/sbin/tini", "--", "node", "app.js" ]
|
||||
|
||||
@@ -1,90 +1,76 @@
|
||||
<p align="center">
|
||||
<img src="./public/static/neteaselogo.png" alt="网易云音乐 API Enhanced Logo" />
|
||||
# 网易云音乐 API
|
||||
|
||||
网易云音乐 Node.js API service
|
||||
|
||||
<p>
|
||||
<a href="https://www.npmjs.com/package/NeteaseCloudMusicApi"><img src="https://img.shields.io/npm/v/NeteaseCloudMusicApi.svg" alt="Version"></a>
|
||||
<a href="https://www.npmjs.com/package/NeteaseCloudMusicApi"><img src="https://img.shields.io/npm/l/NeteaseCloudMusicApi.svg" alt="License"></a>
|
||||
<a href="https://www.npmjs.com/package/NeteaseCloudMusicApi"><img src="https://img.shields.io/david/dev/binaryify/NeteaseCloudMusicApi.svg" alt="devDependencies" ></a>
|
||||
<a href="https://www.npmjs.com/package/NeteaseCloudMusicApi"><img src="https://img.shields.io/david/binaryify/NeteaseCloudMusicApi.svg" alt="devDependencies" ></a>
|
||||
<a href="https://codeclimate.com/github/Binaryify/NeteaseCloudMusicApi"><img src="https://codeclimate.com/github/Binaryify/NeteaseCloudMusicApi/badges/gpa.svg" /></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.npmjs.com/package/@neteasecloudmusicapienhanced/api">
|
||||
<img src="https://img.shields.io/npm/v/@neteasecloudmusicapienhanced/api?style=flat-square" alt="NPM Version" />
|
||||
</a>
|
||||
<a href="https://www.npmjs.com/package/@neteasecloudmusicapienhanced/api">
|
||||
<img src="https://img.shields.io/npm/dm/@neteasecloudmusicapienhanced/api?style=flat-square" alt="NPM Downloads" />
|
||||
</a>
|
||||
<a href="https://github.com/NeteaseCloudMusicApiEnhanced/api-enhanced">
|
||||
<img src="https://img.shields.io/github/license/NeteaseCloudMusicApiEnhanced/api-enhanced?style=flat-square" alt="License" />
|
||||
</a>
|
||||
<a href="https://nodejs.org/">
|
||||
<img src="https://img.shields.io/node/v/@neteasecloudmusicapienhanced/api?style=flat-square" alt="Node Version" />
|
||||
</a>
|
||||
<a href="https://hub.docker.com/r/moefurina/ncm-api">
|
||||
<img src="https://img.shields.io/docker/v/moefurina/ncm-api?style=flat-square" alt="Docker Version" />
|
||||
</a>
|
||||
<a href="https://hub.docker.com/r/moefurina/ncm-api">
|
||||
<img src="https://img.shields.io/docker/pulls/moefurina/ncm-api?style=flat-square" alt="Docker Pulls" />
|
||||
</a>
|
||||
<a href="https://www.typescriptlang.org/">
|
||||
<img src="https://img.shields.io/badge/TypeScript-Ready-blue?style=flat-square&logo=typescript" alt="TypeScript" />
|
||||
</a>
|
||||
</p>
|
||||
## 灵感来自
|
||||
|
||||
[disoul/electron-cloud-music](https://github.com/disoul/electron-cloud-music)
|
||||
|
||||
[darknessomi/musicbox](https://github.com/darknessomi/musicbox)
|
||||
|
||||
## 项目简介
|
||||
[sqaiyan/netmusic-node](https://github.com/sqaiyan/netmusic-node)
|
||||
|
||||
网易云音乐第三方 Node.js API, 支持丰富的音乐相关接口,适合自建服务、二次开发和多平台部署
|
||||
[greats3an/pyncm](https://github.com/greats3an/pyncm)
|
||||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> ## 注意
|
||||
>
|
||||
> - 本项目是自由项目, 不带任何担保, 在基于`MIT License`的许可下, 任何人都可以自由使用、修改和分发本项目的代码, 包括用于商业目的.
|
||||
> - 该项目是个合作计划, 有很多人参与了此次贡献, 前往 [Contributors](https://github.com/NeteaseCloudMusicApiEnhanced/api-enhanced/graphs/contributors) 页面查看所有贡献者
|
||||
> - 原作者项目 [Binaryify/NeteaseCloudMusicApi](https://github.com/binaryify/NeteaseCloudMusicApi) 并非完全停止维护, 你可以在 [NeteaseCloudMusicApi 的 NPMJS 页面](https://www.npmjs.com/package/NeteaseCloudMusicApi) 查看最新版本信息。
|
||||
> - 感谢尊重和理解!
|
||||
## 环境要求
|
||||
|
||||
## 快速开始
|
||||
需要 NodeJS 18+ 环境
|
||||
|
||||
### 环境要求
|
||||
## 安装
|
||||
|
||||
- Node.js 22 及以上
|
||||
- 推荐使用 pnpm 进行依赖管理
|
||||
|
||||
### 安装
|
||||
|
||||
```bash
|
||||
git clone https://github.com/neteasecloudmusicapienhanced/api-enhanced.git
|
||||
cd api-enhanced
|
||||
pnpm i
|
||||
```shell
|
||||
$ git clone git@gitlab.com:Binaryify/NeteaseCloudMusicApi.git
|
||||
$ cd NeteaseCloudMusicApi
|
||||
$ npm install
|
||||
```
|
||||
|
||||
### 启动服务
|
||||
或者
|
||||
|
||||
```bash
|
||||
# 默认端口 3000
|
||||
node app.js
|
||||
|
||||
# 指定端口(如 4000)
|
||||
PORT=4000 node app.js # Mac/Linux
|
||||
set PORT=4000 && node app.js # Windows
|
||||
```shell
|
||||
$ git clone https://gitlab.com/Binaryify/NeteaseCloudMusicApi.git
|
||||
$ cd NeteaseCloudMusicApi
|
||||
$ npm install
|
||||
```
|
||||
|
||||
### 重要提示
|
||||
## 运行
|
||||
|
||||
- 调用前请务必阅读文档的 [调用前须知](https://neteasecloudmusicapienhanced.js.org/#/?id=%e8%b0%83%e7%94%a8%e5%89%8d%e9%a1%bb%e7%9f%a5) 部分。
|
||||
- 推荐将敏感信息(如 cookie)通过部署平台的环境变量进行配置。
|
||||
调用前务必阅读文档的`调用前须知`
|
||||
|
||||
## 在线体验与文档
|
||||
```shell
|
||||
$ node app.js
|
||||
```
|
||||
|
||||
- [在线文档](https://neteasecloudmusicapienhanced.js.org/)
|
||||
- [NPM 包地址](https://www.npmjs.com/package/@neteasecloudmusicapienhanced/api)
|
||||
- [Dockerhub 主页](https://hub.docker.com/r/moefurina/ncm-api)
|
||||
服务器启动默认端口为 3000,若不想使用 3000 端口,可使用以下命令: Mac/Linux
|
||||
|
||||
## 常见部署方式
|
||||
```shell
|
||||
$ PORT=4000 node app.js
|
||||
```
|
||||
|
||||
- 推荐参考[在线文档](https://neteasecloudmusicapienhanced.js.org/)
|
||||
windows 下使用 git-bash 或者 cmder 等终端执行以下命令:
|
||||
|
||||
## Docker 部署说明
|
||||
```shell
|
||||
$ set PORT=4000 && node app.js
|
||||
```
|
||||
|
||||
本项目支持通过 Docker 一键部署,无需手动安装 Node.js 或依赖。
|
||||
## npx 方式运行
|
||||
|
||||
可在终端直接执行
|
||||
|
||||
```bash
|
||||
npx NeteaseCloudMusicApi@latest
|
||||
```
|
||||
|
||||
使用此命令,可直接启动服务,无需下载或者 clone 项目
|
||||
|
||||
## Docker 容器运行
|
||||
|
||||
> 注意: 在 docker 中运行的时候, 由于使用了 request 来发请求, 所以会检查几个 proxy 相关的环境变量(如下所列), 这些环境变量 会影响到 request 的代理, 详情请参考[request 的文档](https://github.com/request/request#proxies), 如果这些环境变量 指向的代理不可用, 那么就会造成错误, 所以在使用 docker 的时候一定要注意这些环境变量. 不过, 要是你在 query 中加上了 proxy 参数, 那么环境变量会被覆盖, 就会用你通过 proxy 参数提供的代理了.
|
||||
|
||||
@@ -98,156 +84,487 @@ request 相关的环境变量
|
||||
6. NO_PROXY
|
||||
|
||||
```shell
|
||||
docker pull moefurina/ncm-api:latest
|
||||
docker pull binaryify/netease_cloud_music_api
|
||||
|
||||
docker run -d -p 3000:3000 --name ncm-api moefurina/ncm-api:latest
|
||||
docker run -d -p 3000:3000 --name netease_cloud_music_api binaryify/netease_cloud_music_api
|
||||
|
||||
|
||||
// 或者
|
||||
docker run -d -p 3000:3000 moefurina/ncm-api:latest
|
||||
docker run -d -p 3000:3000 binaryify/netease_cloud_music_api
|
||||
|
||||
// 去掉或者设置相关的环境变量
|
||||
|
||||
docker run -d -p 3000:3000 --name ncm-api -e http_proxy= -e https_proxy= -e no_proxy= -e HTTP_PROXY= -e HTTPS_PROXY= -e NO_PROXY= moefurina/ncm-api:latest
|
||||
docker run -d -p 3000:3000 --name netease_cloud_music_api -e http_proxy= -e https_proxy= -e no_proxy= -e HTTP_PROXY= -e HTTPS_PROXY= -e NO_PROXY= binaryify/netease_cloud_music_api
|
||||
|
||||
// 或者
|
||||
docker run -d -p 3000:3000 -e http_proxy= -e https_proxy= -e no_proxy= -e HTTP_PROXY= -e HTTPS_PROXY= -e NO_PROXY= moefurina/ncm-api:latest
|
||||
docker run -d -p 3000:3000 -e http_proxy= -e https_proxy= -e no_proxy= -e HTTP_PROXY= -e HTTPS_PROXY= -e NO_PROXY= binaryify/netease_cloud_music_api
|
||||
```
|
||||
|
||||
> 以下是自行 build docker 镜像方式
|
||||
|
||||
```shell
|
||||
$ git clone https://github.com/NeteaseCloudMusicApiEnhanced/api-enhanced && cd api-enhanced
|
||||
```
|
||||
$ git clone https://gitlab.com/Binaryify/neteasecloudmusicapi.git && cd NeteaseCloudMusicApi
|
||||
|
||||
$ sudo docker build . -t ncm-api
|
||||
$ sudo docker build . -t netease-music-api
|
||||
|
||||
$ sudo docker run -d -p 3000:3000 ncm-api
|
||||
$ sudo docker run -d -p 3000:3000 netease-music-api
|
||||
|
||||
## Vercel 部署
|
||||
|
||||
v4.0.8 加入了 Vercel 配置文件,可以直接在 Vercel 下部署了,不需要自己的服务器
|
||||
|
||||
### 操作方法
|
||||
|
||||
1. fork 此项目
|
||||
2. 在 Vercel 官网点击 `New Project`
|
||||
3. 点击 `Import Git Repository` 并选择你 fork 的此项目并点击`import`
|
||||
4. 点击 `PERSONAL ACCOUNT` 的 `select`
|
||||
5. 直接点`Continue`
|
||||
6. `PROJECT NAME`自己填,`FRAMEWORK PRESET` 选 `Other` 然后直接点 `Deploy` 接着等部署完成即可
|
||||
|
||||
## 腾讯云 serverless 部署
|
||||
|
||||
因 `Vercel` 在国内访问太慢,在此提供腾讯云 serverless 部署方法(注意:腾讯云 serverless 并不是免费的,前三个月有免费额度,之后收费)
|
||||
|
||||
### 操作方法
|
||||
|
||||
1. fork 此项目
|
||||
2. 在腾讯云 serverless 应用管理页面( https://console.cloud.tencent.com/sls ),点击`新建应用`
|
||||
3. 顶部`创建方式`选择 `Web 应用`
|
||||
4. 选择 `Express框架`,点击底部`下一步按钮`
|
||||
5. 输入`应用名`,上传方式选择`代码仓库`,进行 GitHub 授权(如已授权可跳过这一步),代码仓库选择刚刚 fork 的项目
|
||||
6. 启动文件填入:
|
||||
|
||||
```
|
||||
#!/bin/bash
|
||||
export PORT=9000
|
||||
/var/lang/node16/bin/node app.js
|
||||
```
|
||||
|
||||
## 3. 环境变量
|
||||
7. 点击`完成`,等待部署完成,点击`资源列表`的 `API网关` 里的 `URL`,正常情况会打开文档地址,点击文档`例子`可查看接口调用效果
|
||||
|
||||
| 变量名 | 默认值 | 说明 |
|
||||
|----------------------------|--------------------------------------|----------------------------------------------------|
|
||||
| **CORS_ALLOW_ORIGIN** | `*` | 允许跨域请求的域名。可填写单个源,或使用逗号分隔多个源(例如 `https://a.com,https://b.com`)。 |
|
||||
| **ENABLE_PROXY** | `false` | 是否启用反向代理功能。 |
|
||||
| **PROXY_URL** | `https://your-proxy-url.com/?proxy=` | 代理服务地址。仅当 `ENABLE_PROXY=true` 时生效。 |
|
||||
| **ENABLE_RANDOM_CN_IP** | `false` | 是否默认启用随机中国IP。启用后,所有请求默认使用随机中国IP,除非请求参数 `randomCNIP` 显式关闭。 |
|
||||
| **ENABLE_GENERAL_UNBLOCK** | `true` | 是否启用全局解灰(推荐开启)。开启后所有歌曲都尝试自动解锁。 |
|
||||
| **ENABLE_FLAC** | `true` | 是否启用无损音质(FLAC)。 |
|
||||
| **SELECT_MAX_BR** | `false` | 启用无损音质时,是否选择最高码率音质。 |
|
||||
| **FOLLOW_SOURCE_ORDER** | `true` | 是否严格按照音源列表顺序进行匹配。 |
|
||||
## 可以在 Node.js 调用
|
||||
|
||||
---
|
||||
|
||||
### 4. 更新镜像
|
||||
|
||||
更新到最新版本:
|
||||
|
||||
```bash
|
||||
docker pull moefurina/ncm-api:latest
|
||||
docker stop ncm-api && docker rm ncm-api
|
||||
docker run -d -p 3000:3000 moefurina/ncm-api:latest
|
||||
```
|
||||
|
||||
## Vercel 一键部署
|
||||
|
||||
1. fork 本项目
|
||||
2. 在 Vercel 官网新建项目,导入 fork 的仓库
|
||||
3. 直接 Deploy
|
||||
|
||||
## 腾讯云 Serverless 部署
|
||||
|
||||
1. fork 本项目
|
||||
2. 在腾讯云 serverless 控制台新建 Web 应用,选择 Express 框架
|
||||
3. 代码仓库选择 fork 的项目,启动文件填写:
|
||||
```bash
|
||||
#!/bin/bash
|
||||
export PORT=9000
|
||||
/var/lang/node16/bin/node app.js
|
||||
```
|
||||
4. 完成部署后,访问 API 网关的 URL 即可
|
||||
|
||||
## Node.js 方式调用
|
||||
|
||||
支持直接在 Node.js 项目中引入和调用,返回 Promise:
|
||||
v3.31.0 后支持 Node.js 调用,导入的方法为`module`内的文件名,返回内容包含`status`和`body`,`status`为状态码,`body`为请求返回内容,参考`module_example` 文件夹下的 `test.js`
|
||||
|
||||
```js
|
||||
const {
|
||||
login_cellphone,
|
||||
user_cloud,
|
||||
} = require('@neteasecloudmusicapienhanced/api')
|
||||
const { login_cellphone, user_cloud } = require('NeteaseCloudMusicApi')
|
||||
async function main() {
|
||||
const result = await login_cellphone({ phone: '手机号', password: '密码' })
|
||||
console.log(result)
|
||||
const result2 = await user_cloud({ cookie: result.body.cookie })
|
||||
console.log(result2.body)
|
||||
try {
|
||||
const result = await login_cellphone({
|
||||
phone: '手机号',
|
||||
password: '密码',
|
||||
})
|
||||
console.log(result)
|
||||
const result2 = await user_cloud({
|
||||
cookie: result.body.cookie, // 凭证
|
||||
})
|
||||
console.log(result2.body)
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
main()
|
||||
```
|
||||
|
||||
## TypeScript 支持
|
||||
## 支持 TypeScript
|
||||
|
||||
```ts
|
||||
import { banner } from '@neteasecloudmusicapienhanced/api'
|
||||
banner({ type: 0 }).then((res) => console.log(res))
|
||||
// test.ts
|
||||
import { banner } from 'NeteaseCloudMusicApi'
|
||||
banner({ type: 0 }).then((res) => {
|
||||
console.log(res)
|
||||
})
|
||||
```
|
||||
|
||||
## 使用文档
|
||||
|
||||
[文档地址](https://docs-neteasecloudmusicapi.vercel.app)
|
||||
|
||||
## 调试工具
|
||||
|
||||
- `eapi` 请求参数或返回内容可在 `/eapi_decrypt.html` 里解析
|
||||
- 请求参数模式下, 解密结果可直接带到 `/api.html` 继续调试
|
||||
- 需要返回值加密时, 可传 `e_r=1`, `weapi` 和 `eapi` 都支持
|
||||
|
||||
## 功能特性
|
||||
|
||||
1. 登录
|
||||
2. 刷新登录
|
||||
3. 发送验证码
|
||||
4. 校验验证码
|
||||
5. 注册(修改密码)
|
||||
6. 获取用户信息 , 歌单,收藏,mv, dj 数量
|
||||
7. 获取用户歌单
|
||||
8. 获取用户电台
|
||||
9. 获取用户关注列表
|
||||
10. 获取用户粉丝列表
|
||||
11. 获取用户动态
|
||||
12. 获取用户播放记录
|
||||
13. 获取精品歌单
|
||||
14. 获取歌单详情
|
||||
15. 搜索
|
||||
16. 搜索建议
|
||||
17. 获取歌词
|
||||
18. 歌曲评论
|
||||
19. 收藏单曲到歌单
|
||||
20. 专辑评论
|
||||
21. 歌单评论
|
||||
22. mv 评论
|
||||
23. 电台节目评论
|
||||
24. banner
|
||||
25. 获取歌曲详情
|
||||
26. 获取专辑内容
|
||||
27. 获取歌手单曲
|
||||
28. 获取歌手 mv
|
||||
29. 获取歌手专辑
|
||||
30. 获取歌手描述
|
||||
31. 获取相似歌手
|
||||
32. 获取相似歌单
|
||||
33. 相似 mv
|
||||
34. 获取相似音乐
|
||||
35. 获取最近 5 个听了这首歌的用户
|
||||
36. 获取每日推荐歌单
|
||||
37. 获取每日推荐歌曲
|
||||
38. 私人 FM
|
||||
39. 签到
|
||||
40. 喜欢音乐
|
||||
41. 垃圾桶
|
||||
42. 歌单 ( 网友精选碟 )
|
||||
43. 新碟上架
|
||||
44. 热门歌手
|
||||
45. 最新 mv
|
||||
46. 推荐 mv
|
||||
47. 推荐歌单
|
||||
48. 推荐新音乐
|
||||
49. 推荐电台
|
||||
50. 推荐节目
|
||||
51. 独家放送
|
||||
52. mv 排行
|
||||
53. 获取 mv 数据
|
||||
54. 播放 mv/视频
|
||||
55. 排行榜
|
||||
56. 歌手榜
|
||||
57. 云盘
|
||||
58. 电台 - 推荐
|
||||
59. 电台 - 分类
|
||||
60. 电台 - 分类推荐
|
||||
61. 电台 - 订阅
|
||||
62. 电台 - 详情
|
||||
63. 电台 - 节目
|
||||
64. 给评论点赞
|
||||
65. 获取动态列表
|
||||
66. 热搜列表(简略)
|
||||
67. 发送私信
|
||||
68. 发送私信歌单
|
||||
69. 新建歌单
|
||||
70. 收藏/取消收藏歌单
|
||||
71. 歌单分类
|
||||
72. 收藏的歌手列表
|
||||
73. 订阅的电台列表
|
||||
74. 相关歌单
|
||||
75. 付费精选接口
|
||||
76. 音乐是否可用检查接口
|
||||
77. 登录状态
|
||||
78. 获取视频播放地址
|
||||
79. 发送/删除评论
|
||||
80. 热门评论
|
||||
81. 视频评论
|
||||
82. 退出登录
|
||||
83. 所有榜单
|
||||
84. 所有榜单内容摘要
|
||||
85. 收藏视频
|
||||
86. 收藏 MV
|
||||
87. 视频详情
|
||||
88. 相关视频
|
||||
89. 关注用户
|
||||
90. 新歌速递
|
||||
91. 喜欢音乐列表(无序)
|
||||
92. 收藏的 MV 列表
|
||||
93. 获取最新专辑
|
||||
94. 听歌打卡
|
||||
95. 获取视频标签/分类下的视频
|
||||
96. 已收藏专辑列表
|
||||
97. 获取动态评论
|
||||
98. 歌单收藏者列表
|
||||
99. 云盘歌曲删除
|
||||
100. 热门话题
|
||||
101. 电台 - 推荐类型
|
||||
102. 电台 - 非热门类型
|
||||
103. 电台 - 今日优选
|
||||
104. 心动模式/智能播放
|
||||
105. 转发动态
|
||||
106. 删除动态
|
||||
107. 分享歌曲、歌单、mv、电台、电台节目到动态
|
||||
108. 通知-私信
|
||||
109. 通知-评论
|
||||
110. 通知-@我
|
||||
111. 通知-通知
|
||||
112. 设置
|
||||
113. 云盘数据详情
|
||||
114. 私信内容
|
||||
115. 我的数字专辑
|
||||
116. batch 批量请求接口
|
||||
117. 获取视频标签列表
|
||||
118. 全部 mv
|
||||
119. 网易出品 mv
|
||||
120. 收藏/取消收藏专辑
|
||||
121. 专辑动态信息
|
||||
122. 热搜列表(详细)
|
||||
123. 更换绑定手机
|
||||
124. 检测手机号码是否已注册
|
||||
125. 初始化昵称
|
||||
126. 更新歌单描述
|
||||
127. 更新歌单名
|
||||
128. 更新歌单标签
|
||||
129. 默认搜索关键词
|
||||
130. 删除歌单
|
||||
131. 电台 banner
|
||||
132. 用户电台
|
||||
133. 热门电台
|
||||
134. 电台 - 节目详情
|
||||
135. 电台 - 节目榜
|
||||
136. 电台 - 新晋电台榜/热门电台榜
|
||||
137. 类别热门电台
|
||||
138. 云村热评(官方下架,暂不能用)
|
||||
139. 电台 24 小时节目榜
|
||||
140. 电台 24 小时主播榜
|
||||
141. 电台最热主播榜
|
||||
142. 电台主播新人榜
|
||||
143. 电台付费精品榜
|
||||
144. 歌手热门 50 首歌曲
|
||||
145. 购买数字专辑
|
||||
146. 获取 mv 点赞转发评论数数据
|
||||
147. 获取视频点赞转发评论数数据
|
||||
148. 调整歌单顺序
|
||||
149. 调整歌曲顺序
|
||||
150. 独家放送列表
|
||||
151. 获取推荐视频
|
||||
152. 获取视频分类列表
|
||||
153. 获取全部视频列表接口
|
||||
154. 获取历史日推可用日期列表
|
||||
155. 获取历史日推详细数据
|
||||
156. 国家编码列表
|
||||
157. 首页-发现
|
||||
158. 首页-发现-圆形图标入口列表
|
||||
159. 全部新碟
|
||||
160. 数字专辑-新碟上架
|
||||
161. 数字专辑&数字单曲-榜单
|
||||
162. 数字专辑-语种风格馆
|
||||
163. 数字专辑详情
|
||||
164. 更新头像
|
||||
165. 歌单封面上传
|
||||
166. 楼层评论
|
||||
167. 歌手全部歌曲
|
||||
168. 精品歌单标签列表
|
||||
169. 用户等级信息
|
||||
170. 电台个性推荐
|
||||
171. 用户绑定信息
|
||||
172. 用户绑定手机
|
||||
173. 新版评论
|
||||
174. 点赞过的视频
|
||||
175. 收藏视频到视频歌单
|
||||
176. 删除视频歌单里的视频
|
||||
177. 最近播放的视频
|
||||
178. 音乐日历
|
||||
179. 电台订阅者列表
|
||||
180. 云贝签到信息
|
||||
181. 云贝签到
|
||||
182. 云贝所有任务
|
||||
183. 云贝 todo 任务
|
||||
184. 云贝今日签到信息
|
||||
185. 云贝完成任务
|
||||
186. 云贝收入
|
||||
187. 云贝支出
|
||||
188. 云贝账户信息
|
||||
189. 账号信息
|
||||
190. 最近联系人
|
||||
191. 私信音乐
|
||||
192. 抱一抱评论
|
||||
193. 评论抱一抱列表
|
||||
194. 收藏的专栏
|
||||
195. 关注歌手新歌
|
||||
196. 关注歌手新 MV
|
||||
197. 歌手详情
|
||||
198. 云盘上传
|
||||
199. 二维码登录
|
||||
200. 话题详情
|
||||
201. 话题详情热门动态
|
||||
202. 歌单详情动态
|
||||
203. 绑定手机
|
||||
204. 一起听状态
|
||||
205. 用户历史评论
|
||||
206. 云盘歌曲信息匹配纠正
|
||||
207. 云贝推歌
|
||||
208. 云贝推歌历史记录
|
||||
209. 已购单曲
|
||||
210. 获取 mlog 播放地址
|
||||
211. 将 mlog id 转为视频 id
|
||||
212. vip 成长值
|
||||
213. vip 成长值获取记录
|
||||
214. vip 任务
|
||||
215. 领取 vip 成长值
|
||||
216. 歌手粉丝
|
||||
217. 数字专辑详情
|
||||
218. 数字专辑销量
|
||||
219. 音乐人数据概况
|
||||
220. 音乐人播放趋势
|
||||
221. 音乐人任务
|
||||
222. 账号云豆数
|
||||
223. 领取云豆
|
||||
224. 获取 VIP 信息
|
||||
225. 音乐人签到
|
||||
226. 获取客户端歌曲下载 url
|
||||
227. 获取歌单所有歌曲
|
||||
228. 乐签信息
|
||||
229. 获取歌手视频
|
||||
230. 最近播放-歌曲
|
||||
231. 最近播放-视频
|
||||
232. 最近播放-声音
|
||||
233. 最近播放-歌单
|
||||
234. 最近播放-专辑
|
||||
235. 最近播放-播客
|
||||
236. 签到进度
|
||||
237. 重复昵称检测
|
||||
238. 歌手粉丝数量
|
||||
239. 音乐人任务(新)
|
||||
240. 内部版本接口
|
||||
241. 歌单更新播放量
|
||||
242. 黑胶时光机
|
||||
243. 音乐百科 - 简要信息
|
||||
244. 乐谱列表
|
||||
245. 乐谱内容
|
||||
246. 曲风列表
|
||||
247. 曲风偏好
|
||||
248. 曲风详情
|
||||
249. 曲风-歌曲
|
||||
250. 曲风-专辑
|
||||
251. 曲风-歌单
|
||||
252. 曲风-歌手
|
||||
253. 私信和通知接口
|
||||
254. 回忆坐标
|
||||
255. 播客搜索
|
||||
256. 播客声音上传
|
||||
257. 验证接口-二维码生成
|
||||
258. 验证接口-二维码检测
|
||||
259. 听歌识曲
|
||||
260. 根据nickname获取userid接口
|
||||
261. 播客声音列表
|
||||
262. 专辑简要百科信息
|
||||
263. 歌曲简要百科信息
|
||||
264. 歌手简要百科信息
|
||||
265. mv简要百科信息
|
||||
266. 搜索歌手
|
||||
267. 用户贡献内容
|
||||
268. 用户贡献条目、积分、云贝数量
|
||||
269. 年度听歌报告
|
||||
270. 播客声音搜索
|
||||
271. 播客声音排序
|
||||
272. 播客列表详情
|
||||
273. 本地歌曲文件匹配网易云歌曲信息
|
||||
274. 歌曲音质详情
|
||||
275. 歌曲红心数量
|
||||
276. 私人 FM 模式选择
|
||||
277. 播客删除
|
||||
278. 电台排行榜获取
|
||||
279. 获取声音歌词
|
||||
280. 获取专辑歌曲的音质
|
||||
281. 歌手动态信息
|
||||
282. 最近听歌列表
|
||||
283. 云盘导入歌曲
|
||||
284. 获取客户端歌曲下载链接 - 新版
|
||||
285. 当前账号关注的用户/歌手
|
||||
286. 会员下载歌曲记录
|
||||
287. 会员本月下载歌曲记录
|
||||
288. 已购买单曲
|
||||
289. 歌曲是否喜爱
|
||||
290. 用户是否互相关注
|
||||
291. 歌曲动态封面
|
||||
292. 用户徽章
|
||||
293. 用户状态
|
||||
294. 用户状态 - 支持设置的状态
|
||||
295. 用户状态 - 相同状态的用户
|
||||
296. 用户状态 - 编辑
|
||||
297. 听歌足迹 - 年度听歌足迹
|
||||
298. 听歌足迹 - 今日收听
|
||||
299. 听歌足迹 - 总收听时长
|
||||
300. 听歌足迹 - 本周/本月收听时长
|
||||
301. 听歌足迹 - 周/月/年收听报告
|
||||
302. 歌单导入 - 元数据/文字/链接导入
|
||||
303. 歌单导入 - 任务状态
|
||||
304. 副歌时间
|
||||
305. 相关歌单推荐
|
||||
306. 歌词摘录 - 歌词摘录信息
|
||||
307. 歌词摘录 - 我的歌词本
|
||||
308. 歌词摘录 - 添加/修改摘录歌词
|
||||
309. 歌词摘录 - 删除摘录歌词
|
||||
310. 广播电台 - 分类/地区信息
|
||||
311. 广播电台 - 我的收藏
|
||||
312. 广播电台 - 电台信息
|
||||
313. 广播电台 - 全部电台
|
||||
314. 广播电台 - 收藏/取消收藏电台
|
||||
315. 用户的创建歌单列表
|
||||
316. 用户的收藏歌单列表
|
||||
317. DIFM电台 - 分类
|
||||
318. DIFM电台 - 收藏列表
|
||||
319. DIFM电台 - 收藏频道
|
||||
320. DIFM电台 - 取消收藏频道
|
||||
321. DIFM电台 - 播放列表
|
||||
322. 助眠解压 - 特定时间场景下的推荐资源
|
||||
323. 助眠解压 - 标签列表
|
||||
324. 助眠解压 - 获取标签下资源列表
|
||||
325. 助眠解压 - 查看同类推荐
|
||||
326. 助眠解压 - 收藏
|
||||
327. 助眠解压 - 收藏列表
|
||||
328. 跑步漫游
|
||||
329. 歌曲创作者信息
|
||||
330. 获取用户详情
|
||||
331. 更新用户信息
|
||||
332. 更新歌单
|
||||
333. 歌手分类列表
|
||||
334. 收藏/取消收藏歌手
|
||||
335. 获取音乐 url
|
||||
336. 获取音乐 url - 新版
|
||||
337. 搜索多重匹配
|
||||
338. 对歌单添加或删除歌曲
|
||||
339. 获取逐字歌词
|
||||
340. 资源点赞( MV,电台,视频)
|
||||
341. 每日推荐歌曲-不感兴趣
|
||||
342. mv 地址
|
||||
343. 排行榜详情
|
||||
344. 电台 - 付费精品
|
||||
345. 电台 - 付费精选
|
||||
346. 发送私信(带专辑)
|
||||
347. 云贝
|
||||
348. 一起听相关
|
||||
349. 获取 VIP 信息(app端)
|
||||
350. 公开隐私歌单
|
||||
351. 云村星评馆 - 简要评论
|
||||
352. 私人 DJ
|
||||
353. 播客列表
|
||||
354. 播客声音详情
|
||||
355. 多级行政区划数据
|
||||
356. 指定维度音乐排行榜详情
|
||||
357. 指定维度音乐排行榜列表
|
||||
|
||||
## 单元测试
|
||||
|
||||
```bash
|
||||
pnpm test
|
||||
```shell
|
||||
$ npm test
|
||||
```
|
||||
|
||||
## 主要功能特性
|
||||
## SDK
|
||||
|
||||
- 登录/注册/验证码
|
||||
- 用户信息、歌单、动态、播放记录
|
||||
- 歌曲、专辑、歌手、MV、歌词、评论、排行榜
|
||||
- 搜索、推荐、私人 FM、签到、云盘
|
||||
- 歌曲解锁(解灰)
|
||||
| 语言 | 作者 | 地址 | 类型 |
|
||||
| :----: | :-----------------------------------------: | :--------------------------------------------------------------------------------------------------------: | :----: |
|
||||
| Java | [JackuXL](https://github.com/JackuXL) | [https://github.com/JackuXL/NeteaseCloudMusicApi-SDK](https://github.com/JackuXL/NeteaseCloudMusicApi-SDK) | 第三方 |
|
||||
| Java | [1015770492](https://github.com/1015770492) | https://github.com/1015770492/yumbo-music-utils | 第三方 |
|
||||
| Python | [盧瞳](https://github.com/2061360308) | [NeteaseCloudMusic_PythonSDK](https://github.com/2061360308/NeteaseCloudMusic_PythonSDK) | 第三方 |
|
||||
|
||||
## 贡献与社区
|
||||
## 贡献者
|
||||
|
||||
- 欢迎提交 PR、Issue 参与维护
|
||||
- 要贡献新接口, 请参考[这篇文章](https://www.focalors.ltd/post/how-to-contribute-ncm-api)
|
||||
|
||||
### 致谢
|
||||
|
||||
原作者 [Binaryify/NeteaseCloudMusicApi](https://github.com/binaryify/NeteaseCloudMusicApi) 项目为本项目基础 (该项目在`npmjs`网站上仍持续维护, 但 github 仓库已不再更新)
|
||||
|
||||
感谢大佬们为逆向eapi, weapi, xeapi等加密算法所做的贡献
|
||||
|
||||
项目参考:
|
||||
|
||||
[trazyn/ieaseMusic](https://github.com/trazyn/ieaseMusic)
|
||||
|
||||
[listen1/listen1_chrome_extension](https://github.com/listen1/listen1_chrome_extension)
|
||||
|
||||
[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 生态
|
||||
|
||||
| 语言 | 作者 | 地址 | 类型 |
|
||||
|--------|---------------------------------------------|------------------------------------------------------------------------------------------|-----|
|
||||
| Java | [JackuXL](https://github.com/JackuXL) | [NeteaseCloudMusicApi-SDK](https://github.com/JackuXL/NeteaseCloudMusicApi-SDK) | 第三方 |
|
||||
| Java | [1015770492](https://github.com/1015770492) | [yumbo-music-utils](https://github.com/1015770492/yumbo-music-utils) | 第三方 |
|
||||
| Python | [盧瞳](https://github.com/2061360308) | [NeteaseCloudMusic_PythonSDK](https://github.com/2061360308/NeteaseCloudMusic_PythonSDK) | 第三方 |
|
||||
| Swift | [Lincb522](https://github.com/Lincb522) | [NeteaseCloudMusicApi-Swift](https://github.com/Lincb522/NeteaseCloudMusicAPI-Swift) | 第三方 |
|
||||
| Rust | [SPlayer-Dev](https://github.com/splayer-dev) | [ncm-api-rs](https://github.com/SPlayer-Dev/ncm-api-rs) | 第三方 |
|
||||
|
||||
|
||||
### 依赖此项目的优秀开源项目
|
||||
|
||||
- [qier2222/YesPlayMusic](https://github.com/qier222/YesPlayMusic)
|
||||
|
||||
- [MaigoLabs/amaoke.app](https://github.com/MaigoLabs/amaoke.app)
|
||||
|
||||
- [Yueby/music-together](https://github.com/Yueby/music-together)
|
||||
|
||||
- [chthollyphlie/folia-major](https://github.com/chthollyphile/folia-major)
|
||||

|
||||
|
||||
## License
|
||||
|
||||
[MIT License](https://github.com/MoeFurina/NeteaseCloudMusicApiEnhanced/blob/main/LICENSE)
|
||||
[The MIT License (MIT)](https://gitlab.com/Binaryify/NeteaseCloudMusicApi/blob/main/LICENSE)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,91 +0,0 @@
|
||||
const { defineConfig, globalIgnores } = require('eslint/config')
|
||||
|
||||
const html = require('eslint-plugin-html')
|
||||
const globals = require('globals')
|
||||
const tsParser = require('@typescript-eslint/parser')
|
||||
const js = require('@eslint/js')
|
||||
|
||||
const { FlatCompat } = require('@eslint/eslintrc')
|
||||
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
recommendedConfig: js.configs.recommended,
|
||||
allConfig: js.configs.all,
|
||||
})
|
||||
|
||||
module.exports = defineConfig([
|
||||
{
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
sourceType: 'module',
|
||||
parserOptions: {
|
||||
parser: 'babel-eslint',
|
||||
},
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node,
|
||||
},
|
||||
},
|
||||
|
||||
plugins: {
|
||||
html,
|
||||
},
|
||||
|
||||
extends: compat.extends('plugin:prettier/recommended'),
|
||||
|
||||
rules: {
|
||||
'prettier/prettier': [
|
||||
'error',
|
||||
{
|
||||
endOfLine: 'auto',
|
||||
},
|
||||
],
|
||||
|
||||
indent: [
|
||||
'error',
|
||||
2,
|
||||
{
|
||||
SwitchCase: 1,
|
||||
},
|
||||
],
|
||||
|
||||
'space-infix-ops': [
|
||||
'error',
|
||||
{
|
||||
int32Hint: false,
|
||||
},
|
||||
],
|
||||
|
||||
'key-spacing': [
|
||||
2,
|
||||
{
|
||||
beforeColon: false,
|
||||
afterColon: true,
|
||||
},
|
||||
],
|
||||
|
||||
'no-octal': 2,
|
||||
'no-redeclare': 2,
|
||||
'comma-spacing': 2,
|
||||
'no-new-object': 2,
|
||||
'arrow-spacing': 2,
|
||||
|
||||
quotes: [
|
||||
2,
|
||||
'single',
|
||||
{
|
||||
avoidEscape: true,
|
||||
allowTemplateLiterals: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
globalIgnores(['**/public/']),
|
||||
{
|
||||
files: ['**/*.ts'],
|
||||
languageOptions: {
|
||||
parser: tsParser,
|
||||
},
|
||||
extends: compat.extends('plugin:@typescript-eslint/recommended'),
|
||||
},
|
||||
])
|
||||
@@ -1,7 +1,6 @@
|
||||
const fsPromises = require('fs/promises')
|
||||
const path = require('path')
|
||||
const server = require('../server')
|
||||
const logger = require('../util/logger.js')
|
||||
|
||||
const exportFile = path.join(__dirname, 'moddef.json')
|
||||
|
||||
@@ -17,7 +16,7 @@ async function main() {
|
||||
)
|
||||
|
||||
fsPromises.writeFile(exportFile, JSON.stringify(def, null, 4))
|
||||
logger.info(`👍 Get your own definition at: ${exportFile}`)
|
||||
console.log(`👍 Get your own definition at: ${exportFile}`)
|
||||
}
|
||||
|
||||
main()
|
||||
|
||||
@@ -2,9 +2,6 @@ const fs = require('fs')
|
||||
const path = require('path')
|
||||
const { register_anonimous } = require('./main')
|
||||
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()
|
||||
|
||||
async function generateConfig() {
|
||||
@@ -23,33 +20,5 @@ async function generateConfig() {
|
||||
} catch (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
|
||||
|
||||
Vendored
-1084
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,29 @@
|
||||
## 环境
|
||||
- 系统/平台: <你的系统和平台>
|
||||
|
||||
- nodejs 版本: <你的 NodeJS 版本号>
|
||||
|
||||
- API版本:<运行的云音乐 API 的版本号, 对应 package.json 里面的 version>
|
||||
|
||||
## 出现问题
|
||||
<出现的问题>
|
||||
|
||||
## 重现步骤
|
||||
<重现步骤>
|
||||
|
||||
## 期待效果
|
||||
<现在的效果,期待的效果>
|
||||
|
||||
|
||||
|
||||
>先看文档有没有相关说明,调用前须知必看
|
||||
|
||||
>先在 issues 搜一下是否有相似问题,没有再发,否则直接关闭
|
||||
|
||||
>不处理别人搭建的线上服务的问题,此项目不提供任何线上服务,请自行搭建
|
||||
|
||||
>重现步骤尽量详细,不能含糊不清,包含请求地址和对应参数以及操作过程描述,不是每个人都喜欢猜别人遇到了什么问题和找参数一个个试,也比较浪费时间
|
||||
|
||||
>如果不是提建议,提 issues 如果不照着模版来将不会优先处理或直接关闭
|
||||
|
||||
>460 cheating 的问题把 `util/request.js` 里面的 `headers['X-Real-IP']` 的注释取消掉就好
|
||||
@@ -1,51 +0,0 @@
|
||||
// 获取广告
|
||||
|
||||
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 },
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
// 获取免费听时长状态
|
||||
|
||||
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'),
|
||||
)
|
||||
}
|
||||
@@ -1,252 +0,0 @@
|
||||
// 看广告免费听歌 - 领取免费听权益
|
||||
// 请求流程(基于逆向网易云音乐 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,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@
|
||||
// 可按需修改此 API 的代码
|
||||
/* {"extInfo":"{\"lastRequestTimestamp\":1692358373509,\"lbsInfoList\":[{\"lat\":40.23076381,\"lon\":129.07545186,\"time\":1692358543},{\"lat\":40.23076381,\"lon\":129.07545186,\"time\":1692055283}],\"listenedTs\":false,\"noAidjToAidj\":true}","header":"{}"} */
|
||||
|
||||
const logger = require('../util/logger.js')
|
||||
const createOption = require('../util/option.js')
|
||||
module.exports = (query, request) => {
|
||||
var extInfo = {}
|
||||
@@ -23,6 +22,6 @@ module.exports = (query, request) => {
|
||||
const data = {
|
||||
extInfo: JSON.stringify(extInfo),
|
||||
}
|
||||
// logger.info(data)
|
||||
// console.log(data)
|
||||
return request(`/api/aidj/content/rcmd/info`, data, createOption(query))
|
||||
}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
// 获取关注歌手的新歌曲和 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'),
|
||||
)
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
// 获取所有关注歌手最近的 50 首新歌
|
||||
|
||||
const createOption = require('../util/option.js')
|
||||
module.exports = (query, request) => {
|
||||
return request(
|
||||
`/api/sub/artist/new/works/song/playall`,
|
||||
{},
|
||||
createOption(query, 'eapi'),
|
||||
)
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
// 发送安全验证码
|
||||
|
||||
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'),
|
||||
)
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
// 发送验证码
|
||||
|
||||
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,9 +6,5 @@ module.exports = (query, request) => {
|
||||
cellphone: query.phone,
|
||||
countrycode: query.countrycode,
|
||||
}
|
||||
return request(
|
||||
`/api/cellphone/existence/check`,
|
||||
data,
|
||||
createOption(query, 'eapi'),
|
||||
)
|
||||
return request(`/api/cellphone/existence/check`, data, createOption(query))
|
||||
}
|
||||
|
||||
+120
-139
@@ -1,26 +1,23 @@
|
||||
const mm = require('music-metadata')
|
||||
const uploadPlugin = require('../plugins/songUpload')
|
||||
const md5 = require('md5')
|
||||
const createOption = require('../util/option.js')
|
||||
const logger = require('../util/logger.js')
|
||||
const {
|
||||
isTempFile,
|
||||
getFileSize,
|
||||
getFileMd5,
|
||||
cleanupTempFile,
|
||||
getFileExtension,
|
||||
sanitizeFilename,
|
||||
} = require('../util/fileHelper')
|
||||
|
||||
let mm
|
||||
module.exports = async (query, request) => {
|
||||
mm = require('music-metadata')
|
||||
|
||||
let ext = 'mp3'
|
||||
// if (query.songFile.name.indexOf('flac') > -1) {
|
||||
// ext = 'flac'
|
||||
// }
|
||||
if (query.songFile.name.includes('.')) {
|
||||
ext = query.songFile.name.split('.').pop()
|
||||
}
|
||||
query.songFile.name = Buffer.from(query.songFile.name, 'latin1').toString(
|
||||
'utf-8',
|
||||
)
|
||||
const ext = getFileExtension(query.songFile.name)
|
||||
const filename = sanitizeFilename(query.songFile.name)
|
||||
const filename = query.songFile.name
|
||||
.replace('.' + ext, '')
|
||||
.replace(/\s/g, '')
|
||||
.replace(/\./g, '_')
|
||||
const bitrate = 999000
|
||||
|
||||
if (!query.songFile) {
|
||||
return Promise.reject({
|
||||
status: 500,
|
||||
@@ -30,135 +27,119 @@ module.exports = async (query, request) => {
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const useTemp = isTempFile(query.songFile)
|
||||
let fileSize = await getFileSize(query.songFile)
|
||||
let fileMd5 = await getFileMd5(query.songFile)
|
||||
|
||||
query.songFile.md5 = fileMd5
|
||||
query.songFile.size = fileSize
|
||||
|
||||
if (!query.songFile.md5) {
|
||||
// 命令行上传没有md5和size信息,需要填充
|
||||
query.songFile.md5 = md5(query.songFile.data)
|
||||
query.songFile.size = query.songFile.data.byteLength
|
||||
}
|
||||
const res = await request(
|
||||
`/api/cloud/upload/check`,
|
||||
{
|
||||
bitrate: String(bitrate),
|
||||
ext: '',
|
||||
length: query.songFile.size,
|
||||
md5: query.songFile.md5,
|
||||
songId: '0',
|
||||
version: 1,
|
||||
},
|
||||
createOption(query),
|
||||
)
|
||||
let artist = ''
|
||||
let album = ''
|
||||
let songName = ''
|
||||
try {
|
||||
const res = await request(
|
||||
`/api/cloud/upload/check`,
|
||||
{
|
||||
bitrate: String(bitrate),
|
||||
ext: '',
|
||||
length: fileSize,
|
||||
md5: fileMd5,
|
||||
songId: '0',
|
||||
version: 1,
|
||||
},
|
||||
createOption(query),
|
||||
const metadata = await mm.parseBuffer(
|
||||
query.songFile.data,
|
||||
query.songFile.mimetype,
|
||||
)
|
||||
const info = metadata.common
|
||||
|
||||
let artist = ''
|
||||
let album = ''
|
||||
let songName = ''
|
||||
|
||||
try {
|
||||
let metadata
|
||||
if (useTemp) {
|
||||
metadata = await mm.parseFile(query.songFile.tempFilePath)
|
||||
} else {
|
||||
metadata = await mm.parseBuffer(
|
||||
query.songFile.data,
|
||||
query.songFile.mimetype,
|
||||
)
|
||||
}
|
||||
const info = metadata.common
|
||||
if (info.title) songName = info.title
|
||||
if (info.album) album = info.album
|
||||
if (info.artist) artist = info.artist
|
||||
} catch (error) {
|
||||
logger.info('元数据解析错误:', error.message)
|
||||
if (info.title) {
|
||||
songName = info.title
|
||||
}
|
||||
|
||||
const tokenRes = await request(
|
||||
`/api/nos/token/alloc`,
|
||||
{
|
||||
bucket: '',
|
||||
ext: ext,
|
||||
filename: filename,
|
||||
local: false,
|
||||
nos_product: 3,
|
||||
type: 'audio',
|
||||
md5: fileMd5,
|
||||
},
|
||||
createOption(query),
|
||||
)
|
||||
|
||||
if (!tokenRes.body.result || !tokenRes.body.result.resourceId) {
|
||||
logger.error('Token分配失败:', tokenRes.body)
|
||||
return Promise.reject({
|
||||
status: 500,
|
||||
body: {
|
||||
code: 500,
|
||||
msg: '获取上传token失败',
|
||||
detail: tokenRes.body,
|
||||
},
|
||||
})
|
||||
if (info.album) {
|
||||
album = info.album
|
||||
}
|
||||
|
||||
if (res.body.needUpload) {
|
||||
logger.info('需要上传,开始上传流程...')
|
||||
try {
|
||||
const uploadInfo = await uploadPlugin(query, request)
|
||||
logger.info('上传完成:', uploadInfo?.body?.result?.resourceId)
|
||||
} catch (uploadError) {
|
||||
logger.error('上传失败:', uploadError)
|
||||
return Promise.reject(uploadError)
|
||||
}
|
||||
} else {
|
||||
logger.info('文件已存在,跳过上传')
|
||||
if (info.artist) {
|
||||
artist = info.artist
|
||||
}
|
||||
// if (metadata.native.ID3v1) {
|
||||
// metadata.native.ID3v1.forEach((item) => {
|
||||
// // console.log(item.id, item.value)
|
||||
// if (item.id === 'title') {
|
||||
// songName = item.value
|
||||
// }
|
||||
// if (item.id === 'artist') {
|
||||
// artist = item.value
|
||||
// }
|
||||
// if (item.id === 'album') {
|
||||
// album = item.value
|
||||
// }
|
||||
// })
|
||||
// // console.log({
|
||||
// // songName,
|
||||
// // album,
|
||||
// // songName,
|
||||
// // })
|
||||
// }
|
||||
// console.log({
|
||||
// songName,
|
||||
// album,
|
||||
// songName,
|
||||
// })
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
const tokenRes = await request(
|
||||
`/api/nos/token/alloc`,
|
||||
{
|
||||
bucket: '',
|
||||
ext: ext,
|
||||
filename: filename,
|
||||
local: false,
|
||||
nos_product: 3,
|
||||
type: 'audio',
|
||||
md5: query.songFile.md5,
|
||||
},
|
||||
createOption(query),
|
||||
)
|
||||
|
||||
const res2 = await request(
|
||||
`/api/upload/cloud/info/v2`,
|
||||
{
|
||||
md5: fileMd5,
|
||||
songid: res.body.songId,
|
||||
filename: query.songFile.name,
|
||||
song: songName || filename,
|
||||
album: album || '未知专辑',
|
||||
artist: artist || '未知艺术家',
|
||||
bitrate: String(bitrate),
|
||||
resourceId: tokenRes.body.result.resourceId,
|
||||
},
|
||||
createOption(query),
|
||||
)
|
||||
|
||||
if (res2.body.code !== 200) {
|
||||
logger.error('云盘信息上传失败:', res2.body)
|
||||
return Promise.reject({
|
||||
status: res2.status || 500,
|
||||
body: {
|
||||
code: res2.body.code || 500,
|
||||
msg: res2.body.msg || '上传云盘信息失败',
|
||||
detail: res2.body,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const res3 = await request(
|
||||
`/api/cloud/pub/v2`,
|
||||
{
|
||||
songid: res2.body.songId,
|
||||
},
|
||||
createOption(query),
|
||||
)
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
...res.body,
|
||||
...res3.body,
|
||||
},
|
||||
cookie: res.cookie,
|
||||
}
|
||||
} finally {
|
||||
if (useTemp) {
|
||||
await cleanupTempFile(query.songFile.tempFilePath)
|
||||
}
|
||||
if (res.body.needUpload) {
|
||||
const uploadInfo = await uploadPlugin(query, request)
|
||||
// console.log('uploadInfo', uploadInfo.body.result.resourceId)
|
||||
}
|
||||
// console.log(tokenRes.body.result)
|
||||
const res2 = await request(
|
||||
`/api/upload/cloud/info/v2`,
|
||||
{
|
||||
md5: query.songFile.md5,
|
||||
songid: res.body.songId,
|
||||
filename: query.songFile.name,
|
||||
song: songName || filename,
|
||||
album: album || '未知专辑',
|
||||
artist: artist || '未知艺术家',
|
||||
bitrate: String(bitrate),
|
||||
resourceId: tokenRes.body.result.resourceId,
|
||||
},
|
||||
createOption(query),
|
||||
)
|
||||
// console.log({ res2, privateCloud: res2.body.privateCloud })
|
||||
// console.log(res.body.songId, 'songid')
|
||||
const res3 = await request(
|
||||
`/api/cloud/pub/v2`,
|
||||
{
|
||||
songid: res2.body.songId,
|
||||
},
|
||||
createOption(query),
|
||||
)
|
||||
// console.log({ res3 })
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
...res.body,
|
||||
...res3.body,
|
||||
// ...uploadInfo,
|
||||
},
|
||||
cookie: res.cookie,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
// 获取云盘歌词
|
||||
const createOption = require('../util/option.js')
|
||||
module.exports = (query, request) => {
|
||||
const data = {
|
||||
userId: query.uid,
|
||||
songId: query.sid,
|
||||
lv: -1,
|
||||
kv: -1,
|
||||
}
|
||||
return request(`/api/cloud/lyric/get`, data, createOption(query, 'eapi'))
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
const createOption = require('../util/option.js')
|
||||
|
||||
module.exports = async (query, request) => {
|
||||
const {
|
||||
songId,
|
||||
resourceId,
|
||||
md5,
|
||||
filename,
|
||||
song,
|
||||
artist,
|
||||
album,
|
||||
bitrate = 999000,
|
||||
} = query
|
||||
|
||||
if (!songId || !resourceId || !md5 || !filename) {
|
||||
return Promise.reject({
|
||||
status: 400,
|
||||
body: {
|
||||
code: 400,
|
||||
msg: '缺少必要参数: songId, resourceId, md5, filename',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const songName = song || filename.replace(/\.[^.]+$/, '')
|
||||
|
||||
const res2 = await request(
|
||||
`/api/upload/cloud/info/v2`,
|
||||
{
|
||||
md5: md5,
|
||||
songid: songId,
|
||||
filename: filename,
|
||||
song: songName,
|
||||
album: album || '未知专辑',
|
||||
artist: artist || '未知艺术家',
|
||||
bitrate: String(bitrate),
|
||||
resourceId: resourceId,
|
||||
},
|
||||
createOption(query),
|
||||
)
|
||||
|
||||
if (res2.body.code !== 200) {
|
||||
return Promise.reject({
|
||||
status: res2.status || 500,
|
||||
body: {
|
||||
code: res2.body.code || 500,
|
||||
msg: res2.body.msg || '上传云盘信息失败',
|
||||
detail: res2.body,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const res3 = await request(
|
||||
`/api/cloud/pub/v2`,
|
||||
{
|
||||
songid: res2.body.songId,
|
||||
},
|
||||
createOption(query),
|
||||
)
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
code: 200,
|
||||
data: {
|
||||
songId: res2.body.songId,
|
||||
...res3.body,
|
||||
},
|
||||
},
|
||||
cookie: res2.cookie,
|
||||
}
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
const { default: axios } = require('axios')
|
||||
const createOption = require('../util/option.js')
|
||||
|
||||
module.exports = async (query, request) => {
|
||||
const { md5, fileSize, filename, bitrate = 999000 } = query
|
||||
|
||||
if (!md5 || !fileSize || !filename) {
|
||||
return Promise.reject({
|
||||
status: 400,
|
||||
body: {
|
||||
code: 400,
|
||||
msg: '缺少必要参数: md5, fileSize, filename',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const ext = filename.includes('.') ? filename.split('.').pop() : 'mp3'
|
||||
|
||||
const checkRes = await request(
|
||||
`/api/cloud/upload/check`,
|
||||
{
|
||||
bitrate: String(bitrate),
|
||||
ext: '',
|
||||
length: fileSize,
|
||||
md5: md5,
|
||||
songId: '0',
|
||||
version: 1,
|
||||
},
|
||||
createOption(query),
|
||||
)
|
||||
|
||||
const bucket = 'jd-musicrep-privatecloud-audio-public'
|
||||
const tokenRes = await request(
|
||||
`/api/nos/token/alloc`,
|
||||
{
|
||||
bucket: bucket,
|
||||
ext: ext,
|
||||
filename: filename
|
||||
.replace(/\.[^.]+$/, '')
|
||||
.replace(/\s/g, '')
|
||||
.replace(/\./g, '_'),
|
||||
local: false,
|
||||
nos_product: 3,
|
||||
type: 'audio',
|
||||
md5: md5,
|
||||
},
|
||||
createOption(query, 'weapi'),
|
||||
)
|
||||
|
||||
if (!tokenRes.body.result || !tokenRes.body.result.objectKey) {
|
||||
return Promise.reject({
|
||||
status: 500,
|
||||
body: {
|
||||
code: 500,
|
||||
msg: '获取上传token失败',
|
||||
detail: tokenRes.body,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
let lbs
|
||||
try {
|
||||
lbs = (
|
||||
await axios({
|
||||
method: 'get',
|
||||
url: `https://wanproxy.127.net/lbs?version=1.0&bucketname=${bucket}`,
|
||||
timeout: 10000,
|
||||
})
|
||||
).data
|
||||
} catch (error) {
|
||||
return Promise.reject({
|
||||
status: 500,
|
||||
body: {
|
||||
code: 500,
|
||||
msg: '获取上传服务器地址失败',
|
||||
detail: error.message,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if (!lbs || !lbs.upload || !lbs.upload[0]) {
|
||||
return Promise.reject({
|
||||
status: 500,
|
||||
body: {
|
||||
code: 500,
|
||||
msg: '获取上传服务器地址无效',
|
||||
detail: lbs,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
code: 200,
|
||||
data: {
|
||||
needUpload: checkRes.body.needUpload,
|
||||
songId: checkRes.body.songId,
|
||||
uploadToken: tokenRes.body.result.token,
|
||||
objectKey: tokenRes.body.result.objectKey,
|
||||
resourceId: tokenRes.body.result.resourceId,
|
||||
uploadUrl: `${lbs.upload[0]}/${bucket}/${tokenRes.body.result.objectKey.replace(/\//g, '%2F')}?offset=0&complete=true&version=1.0`,
|
||||
bucket: bucket,
|
||||
md5: md5,
|
||||
fileSize: fileSize,
|
||||
filename: filename,
|
||||
},
|
||||
},
|
||||
cookie: checkRes.cookie,
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -25,6 +25,6 @@ module.exports = (query, request) => {
|
||||
return request(
|
||||
`/api/resource/comments/${query.t}`,
|
||||
data,
|
||||
createOption(query, 'eapi', 'v2'),
|
||||
createOption(query, 'weapi'),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
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'),
|
||||
)
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
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'),
|
||||
)
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
// 评论统计数据
|
||||
// type: 0=歌曲 1=MV 2=歌单 3=专辑 4=电台节目 5=视频 6=动态 7=电台
|
||||
// ids: 资源 ID 列表,多个用逗号分隔,如 "123,456"
|
||||
const { resourceTypeMap } = require('../util/config.json')
|
||||
const createOption = require('../util/option.js')
|
||||
|
||||
// 从 resourceTypeMap 的前缀值中提取网易云内部资源类型编号
|
||||
// 例如 "R_SO_4_" -> "4", "A_DR_14_" -> "14"
|
||||
const resourceTypeIdMap = Object.fromEntries(
|
||||
Object.entries(resourceTypeMap).map(([key, prefix]) => [
|
||||
key,
|
||||
prefix.replace(/_$/, '').split('_').pop(),
|
||||
]),
|
||||
)
|
||||
|
||||
module.exports = (query, request) => {
|
||||
const ids = String(query.ids || query.id || '')
|
||||
.split(',')
|
||||
.map((id) => id.trim())
|
||||
.filter(Boolean)
|
||||
|
||||
return request(
|
||||
`/api/resource/commentInfo/list`,
|
||||
{
|
||||
resourceType: resourceTypeIdMap[String(query.type || 0)],
|
||||
resourceIds: JSON.stringify(ids),
|
||||
},
|
||||
createOption(query, 'weapi'),
|
||||
)
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
const { resourceTypeMap } = require('../util/config.json')
|
||||
// 发送评论
|
||||
|
||||
const createOption = require('../util/option.js')
|
||||
module.exports = (query, request) => {
|
||||
const data = {
|
||||
threadId: resourceTypeMap[query.type] + query.id,
|
||||
commentId: query.cid,
|
||||
content: query.content,
|
||||
resourceType: '0',
|
||||
}
|
||||
return request(
|
||||
`/api/v1/resource/comments/reply`,
|
||||
data,
|
||||
createOption(query, 'xeapi', 'v3'),
|
||||
)
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
// 举报评论
|
||||
|
||||
const createOption = require('../util/option.js')
|
||||
module.exports = (query, request) => {
|
||||
const data = {
|
||||
threadId: 'R_SO_4_' + query.id,
|
||||
commentId: query.cid,
|
||||
reason: query.reason,
|
||||
}
|
||||
return request(`/api/report/reportcomment`, data, createOption(query))
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
const {
|
||||
eapiResDecrypt,
|
||||
eapiReqDecrypt,
|
||||
aesDecrypt,
|
||||
xeapiResDecrypt,
|
||||
} = require('../util/crypto')
|
||||
const CryptoJS = require('crypto-js')
|
||||
|
||||
const linuxapiKey = 'rFgB&h#%2?^eDg:Q'
|
||||
|
||||
module.exports = async (query, request) => {
|
||||
const crypto = query.crypto || 'eapi'
|
||||
const data = query.data || query.hexString || ''
|
||||
const isReq = query.isReq !== 'false'
|
||||
|
||||
if (!data) {
|
||||
return {
|
||||
status: 400,
|
||||
body: { code: 400, message: 'data is required' },
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
let result
|
||||
switch (crypto) {
|
||||
case 'eapi': {
|
||||
const pureHex = data.replace(/\s/g, '')
|
||||
result = isReq ? eapiReqDecrypt(pureHex) : eapiResDecrypt(pureHex)
|
||||
break
|
||||
}
|
||||
|
||||
case 'weapi': {
|
||||
if (isReq) {
|
||||
return {
|
||||
status: 400,
|
||||
body: {
|
||||
code: 400,
|
||||
message:
|
||||
'weapi 请求解密需要 RSA 私钥,暂不支持;仅支持 weapi 返回数据解密(e_r=true 时与 eapi 相同)',
|
||||
},
|
||||
}
|
||||
}
|
||||
const pureHex = data.replace(/\s/g, '')
|
||||
result = eapiResDecrypt(pureHex)
|
||||
break
|
||||
}
|
||||
|
||||
case 'linuxapi': {
|
||||
if (isReq) {
|
||||
const pureHex = data.replace(/\s/g, '')
|
||||
const decrypted = aesDecrypt(pureHex, '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}` },
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
// 强制下线设备
|
||||
|
||||
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'),
|
||||
)
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
// 登录设备列表
|
||||
|
||||
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'),
|
||||
)
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
// 上报设备中心设备名称
|
||||
// 官方 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'),
|
||||
)
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
// 修改本人动态的可见权限
|
||||
|
||||
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))
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
// 乐迷团详情
|
||||
|
||||
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'),
|
||||
)
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
// 乐迷团推荐笔记
|
||||
|
||||
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'),
|
||||
)
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
// 用户所处乐迷团详情
|
||||
|
||||
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'),
|
||||
)
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
// 获取当前登录用户加入的全部歌手乐迷团列表
|
||||
|
||||
const createOption = require('../util/option.js')
|
||||
|
||||
module.exports = (query, request) => {
|
||||
return request(
|
||||
'/api/social/fansgroup/bff/user/groups/get',
|
||||
{},
|
||||
createOption(query, 'eapi'),
|
||||
)
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
// 红心与取消红心歌曲- 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'))
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
// 听歌足迹 - 歌曲播放排行 (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,7 +14,6 @@ module.exports = async (query, request) => {
|
||||
? query.captcha
|
||||
: query.md5_password || CryptoJS.MD5(query.password).toString(),
|
||||
remember: 'true',
|
||||
secureCaptcha: query.sca || '',
|
||||
}
|
||||
let result = await request(
|
||||
`/api/w/login/cellphone`,
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
// 云小编每日抽奖
|
||||
//
|
||||
// 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'),
|
||||
)
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
// 云小编抽奖剩余次数查询
|
||||
//
|
||||
// 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,11 +0,0 @@
|
||||
// 获取音乐人任务
|
||||
|
||||
const createOption = require('../util/option.js')
|
||||
module.exports = (query, request) => {
|
||||
const data = {}
|
||||
return request(
|
||||
`/api/nmusician/workbench/special/right/vip/info`,
|
||||
data,
|
||||
createOption(query, 'eapi'),
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
// 私人FM - 模式选择
|
||||
|
||||
// aidj, DEFAULT, FAMILIAR, EXPLORE, SCENE_RCMD ( EXERCISE, FOCUS, NIGHT_EMO )
|
||||
// 来不及解释这几个了
|
||||
|
||||
const createOption = require('../util/option.js')
|
||||
module.exports = (query, request) => {
|
||||
const data = {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// 私信和通知接口
|
||||
|
||||
const createOption = require('../util/option.js')
|
||||
module.exports = (query, request) => {
|
||||
const data = {}
|
||||
|
||||
@@ -9,6 +9,6 @@ module.exports = (query, request) => {
|
||||
? { checkToken: query.checkToken || APP_CONF.checkToken }
|
||||
: {}),
|
||||
}
|
||||
query.checkToken = 'v2' // 强制开启checkToken
|
||||
query.checkToken = true // 强制开启checkToken
|
||||
return request(`/api/playlist/${path}`, data, createOption(query, 'eapi'))
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
const createOption = require('../util/option.js')
|
||||
const logger = require('../util/logger.js')
|
||||
module.exports = async (query, request) => {
|
||||
query.ids = query.ids || ''
|
||||
const data = {
|
||||
@@ -10,7 +9,7 @@ module.exports = async (query, request) => {
|
||||
}),
|
||||
),
|
||||
}
|
||||
logger.info(data)
|
||||
console.log(data)
|
||||
|
||||
return request(`/api/playlist/track/add`, data, createOption(query, 'weapi'))
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
// 每日风格推荐场景配置列表获取
|
||||
|
||||
const createOption = require('../util/option.js')
|
||||
module.exports = (query, request) => {
|
||||
const data = {}
|
||||
return request(
|
||||
`/api/homepage/daily/song/config/get`,
|
||||
data,
|
||||
createOption(query, 'xeapi'),
|
||||
)
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
// 每日风格推荐场景配置设置
|
||||
|
||||
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'),
|
||||
)
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
// 每日推荐风格歌曲
|
||||
|
||||
const createOption = require('../util/option.js')
|
||||
module.exports = (query, request) => {
|
||||
const data = {}
|
||||
return request(
|
||||
`/api/homepage/category/daily/song/list`,
|
||||
data,
|
||||
createOption(query, 'xeapi'),
|
||||
)
|
||||
}
|
||||
@@ -2,9 +2,7 @@
|
||||
|
||||
const createOption = require('../util/option.js')
|
||||
module.exports = (query, request) => {
|
||||
const data = {
|
||||
afresh: query.afresh,
|
||||
}
|
||||
const data = {}
|
||||
return request(
|
||||
`/api/v3/discovery/recommend/songs`,
|
||||
data,
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
// 每日推荐歌曲 - 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,10 +1,7 @@
|
||||
// 获取游客cookie
|
||||
|
||||
const CryptoJS = require('crypto-js')
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
const ID_XOR_KEY_1 = '3go8&$8*3*3h0k(2)2'
|
||||
const logger = require('../util/logger.js')
|
||||
|
||||
const createOption = require('../util/option.js')
|
||||
const { generateDeviceId } = require('../util/index')
|
||||
@@ -26,7 +23,7 @@ function cloudmusic_dll_encode_id(some_id) {
|
||||
|
||||
module.exports = async (query, request) => {
|
||||
const deviceId = generateDeviceId()
|
||||
logger.info(`Successfully registered anonimous token, deviceId: ${deviceId}`)
|
||||
console.log(`[register_anonimous] deviceId: ${deviceId}`)
|
||||
global.deviceId = deviceId
|
||||
const encodedId = CryptoJS.enc.Base64.stringify(
|
||||
CryptoJS.enc.Utf8.parse(
|
||||
@@ -39,7 +36,7 @@ module.exports = async (query, request) => {
|
||||
let result = await request(
|
||||
`/api/register/anonimous`,
|
||||
data,
|
||||
createOption(query, 'xeapi'),
|
||||
createOption(query, 'weapi'),
|
||||
)
|
||||
if (result.body.code === 200) {
|
||||
result = {
|
||||
|
||||
@@ -1,158 +0,0 @@
|
||||
// 易盾反作弊 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 ''
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
// 易盾反作弊 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 ''
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
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: [],
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
const { default: axios } = require('axios')
|
||||
const encrypt = require('../util/crypto')
|
||||
const { APP_CONF } = require('../util/config.json')
|
||||
|
||||
const generateNonce = () => {
|
||||
let nonce = ''
|
||||
for (let i = 0; i < 16; i++) {
|
||||
nonce += Math.floor(Math.random() * 10).toString()
|
||||
}
|
||||
return nonce
|
||||
}
|
||||
|
||||
module.exports = async (query, request) => {
|
||||
const nonce = generateNonce()
|
||||
const timestamp = String(Date.now())
|
||||
const deviceId = query.deviceId || global.deviceId || ''
|
||||
const currentKeyVersion = query.currentKeyVersion || ''
|
||||
|
||||
const data = {
|
||||
appVersion: '9.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: [],
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
// 提交歌曲播放状态
|
||||
|
||||
const createOption = require('../util/option.js')
|
||||
const generateSessionId = () =>
|
||||
Array.from(
|
||||
{ length: 12 },
|
||||
() =>
|
||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'[Math.floor(Math.random() * 36)],
|
||||
).join('')
|
||||
|
||||
module.exports = (query, request) => {
|
||||
const {
|
||||
id,
|
||||
sessionId,
|
||||
progress = 0,
|
||||
playMode = 'list_loop',
|
||||
type = 'song',
|
||||
} = query
|
||||
|
||||
if (!id) {
|
||||
return Promise.reject({
|
||||
status: 400,
|
||||
body: {
|
||||
code: 400,
|
||||
msg: '缺少必要参数:id',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const playStateSubmitReq = JSON.stringify({
|
||||
resource: {
|
||||
id: String(id),
|
||||
type: type,
|
||||
},
|
||||
progress: Number(progress) || 0,
|
||||
sessionId: sessionId || generateSessionId(),
|
||||
playMode: playMode,
|
||||
})
|
||||
|
||||
const data = {
|
||||
playStateSubmitReq: playStateSubmitReq,
|
||||
}
|
||||
|
||||
return request(
|
||||
'/api/relay/play/state/submit',
|
||||
data,
|
||||
createOption(query, 'weapi'),
|
||||
)
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
// 云小编领取任务积分
|
||||
//
|
||||
// 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'),
|
||||
)
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
// 云小编活动信息
|
||||
|
||||
const createOption = require('../util/option.js')
|
||||
|
||||
module.exports = (query, request) => {
|
||||
return request(`/api/rep/ugc/activity/get`, {}, createOption(query, 'eapi'))
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
// 云小编考试状态
|
||||
//
|
||||
// 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'),
|
||||
)
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
// 云小编考试取题
|
||||
//
|
||||
// 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'),
|
||||
)
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
// 云小编考试结果
|
||||
//
|
||||
// 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'),
|
||||
)
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
// 云小编考试开始
|
||||
//
|
||||
// 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'))
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
// 云小编考试提交
|
||||
//
|
||||
// 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'))
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
// 云小编领取一日会员
|
||||
//
|
||||
// 注:前提条件见 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'),
|
||||
)
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
// 云小编获取用户详情
|
||||
|
||||
const createOption = require('../util/option.js')
|
||||
|
||||
module.exports = (query, request) => {
|
||||
return request(`/api/rep/ugc/user/get`, {}, createOption(query, 'eapi'))
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
// 云小编每日签到
|
||||
|
||||
const createOption = require('../util/option.js')
|
||||
|
||||
module.exports = (query, request) => {
|
||||
return request(`/api/rep/ugc/user/sign`, {}, createOption(query, 'eapi'))
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
// 云小编查询会员任务状态
|
||||
//
|
||||
// 状态 (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'))
|
||||
}
|
||||
+5
-57
@@ -1,42 +1,8 @@
|
||||
// 听歌打卡
|
||||
|
||||
const createOption = require('../util/option.js')
|
||||
const { APP_CONF } = require('../util/config.json')
|
||||
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 = {
|
||||
module.exports = (query, request) => {
|
||||
const data = {
|
||||
logs: JSON.stringify([
|
||||
{
|
||||
action: 'play',
|
||||
@@ -49,30 +15,12 @@ module.exports = async (query, request) => {
|
||||
type: 'song',
|
||||
wifi: 0,
|
||||
source: 'list',
|
||||
mainsite: '1',
|
||||
mainsiteWeb: '1',
|
||||
content: `id=${query.sourceid}`,
|
||||
mainsite: 1,
|
||||
content: '',
|
||||
},
|
||||
},
|
||||
]),
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
},
|
||||
}
|
||||
return request(`/api/feedback/weblog`, data, createOption(query, 'weapi'))
|
||||
}
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
// 听歌打卡 - NCBL 加密版 (仿桌面客户端 PLV/PLD 上报)
|
||||
// 复制自 https://github.com/folltoshe/netease-report-listen-song 的 PC 端日志上报方式
|
||||
//
|
||||
// PLV 和 PLD 分两次独立上传
|
||||
|
||||
const {
|
||||
buildPlv,
|
||||
buildPld,
|
||||
buildRecords,
|
||||
extractContext,
|
||||
parseCookie,
|
||||
buildCookieStr,
|
||||
buildMetaJson,
|
||||
doUpload,
|
||||
} = require('../util/ncbl')
|
||||
|
||||
module.exports = async (query, request) => {
|
||||
// --- 参数校验 ---
|
||||
const songId = Number(query.id)
|
||||
if (!songId || isNaN(songId)) {
|
||||
return { status: 400, body: { code: 400, msg: '缺少有效的 id (歌曲ID)' } }
|
||||
}
|
||||
const playTime = Number(query.time)
|
||||
if (isNaN(playTime) || playTime <= 0) {
|
||||
return {
|
||||
status: 400,
|
||||
body: { code: 400, msg: '缺少有效的 time (播放时长)' },
|
||||
}
|
||||
}
|
||||
const totalTime = Number(query.total) || playTime
|
||||
const sourceId = String(query.sourceid || query.sourceId || '')
|
||||
const sourceName = query.source || 'list'
|
||||
|
||||
// --- 解析认证上下文 ---
|
||||
const rawCookie = query.cookie || ''
|
||||
const cookieObj = parseCookie(rawCookie)
|
||||
cookieObj.os = 'pc'
|
||||
const ctx = extractContext(cookieObj)
|
||||
|
||||
// 兜底取 token
|
||||
if (!ctx.auth.token && rawCookie) {
|
||||
const parsed =
|
||||
typeof rawCookie === 'string' ? parseCookie(rawCookie) : rawCookie
|
||||
ctx.auth.token = parsed.MUSIC_U || ''
|
||||
}
|
||||
|
||||
if (!ctx.auth.token) {
|
||||
return { status: 401, body: { code: 401, msg: '缺少 MUSIC_U 鉴权令牌' } }
|
||||
}
|
||||
|
||||
// --- 构建歌曲和来源 ---
|
||||
const song = {
|
||||
id: songId,
|
||||
name: query.name || '',
|
||||
artist: query.artist || '',
|
||||
bitrate: Number(query.bitrate) || 320,
|
||||
level: query.level || 'exhigh',
|
||||
vip: query.vip === 'true' || query.vip === true,
|
||||
time: totalTime,
|
||||
}
|
||||
const source = {
|
||||
id: sourceId || String(songId),
|
||||
type: 'track',
|
||||
name: sourceName,
|
||||
}
|
||||
|
||||
const metaJson = buildMetaJson(ctx)
|
||||
const cookieStr = buildCookieStr(ctx)
|
||||
|
||||
const ts = Math.floor(Date.now() / 1000)
|
||||
const played = Math.min(playTime, totalTime)
|
||||
|
||||
const plvBody = buildRecords([
|
||||
{ time: ts, action: '_plv', data: buildPlv(ctx, song, source) },
|
||||
])
|
||||
const pldBody = buildRecords([
|
||||
{ time: ts, action: '_pld', data: buildPld(ctx, song, source, played) },
|
||||
])
|
||||
|
||||
try {
|
||||
// 1) 上传 PLV
|
||||
const plv = await doUpload(ctx, metaJson, plvBody, cookieStr, 'PLV')
|
||||
if (!plv.success) {
|
||||
const rateMsg =
|
||||
plv.respBody?.data?.rate != null
|
||||
? ` (rate=${plv.respBody.data.rate})`
|
||||
: ''
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
code: plv.respBody?.code || -1,
|
||||
msg: `PLV 上报失败${rateMsg}`,
|
||||
details: plv.respBody,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// 2) 上传 PLD
|
||||
const pld = await doUpload(ctx, metaJson, pldBody, cookieStr, 'PLD')
|
||||
if (!pld.success) {
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
code: pld.respBody?.code || -1,
|
||||
msg: 'PLV 成功但 PLD 失败',
|
||||
details: { plv: plv.respBody, pld: pld.respBody },
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
code: 200,
|
||||
data: 'scrobble_v1 上报成功',
|
||||
details: {
|
||||
plv: { fileName: plv.fileName, payloadSize: plv.payload.length },
|
||||
pld: { fileName: pld.fileName, payloadSize: pld.payload.length },
|
||||
},
|
||||
},
|
||||
}
|
||||
} catch (err) {
|
||||
return {
|
||||
status: 502,
|
||||
body: { code: 502, msg: `请求异常: ${err.message || err}` },
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
// 搜索建议pc端
|
||||
|
||||
const createOption = require('../util/option.js')
|
||||
module.exports = (query, request) => {
|
||||
const data = {
|
||||
keyword: query.keyword || '',
|
||||
}
|
||||
return request(
|
||||
`/api/search/pc/suggest/keyword/get`,
|
||||
data,
|
||||
createOption(query),
|
||||
)
|
||||
}
|
||||
@@ -7,9 +7,5 @@ module.exports = (query, request) => {
|
||||
msg: query.msg || '',
|
||||
id: query.id || '',
|
||||
}
|
||||
return request(
|
||||
`/api/share/friends/resource`,
|
||||
data,
|
||||
createOption(query, 'xeapi', 'v3'),
|
||||
)
|
||||
return request(`/api/share/friends/resource`, data, createOption(query))
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
// 从云盘获取歌曲下载链接
|
||||
|
||||
const createOption = require('../util/option.js')
|
||||
module.exports = (query, request) => {
|
||||
const data = {
|
||||
songId: query.id,
|
||||
}
|
||||
return request(`/api/cloud/dowonload`, data, createOption(query, 'eapi'))
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
// 灰色歌曲的其他版本推荐
|
||||
|
||||
const createOption = require('../util/option.js')
|
||||
module.exports = (query, request) => {
|
||||
const data = {
|
||||
songid: query.songid || query.id,
|
||||
}
|
||||
return request(`/api/song/copyright/rcmd`, data, createOption(query, 'eapi'))
|
||||
}
|
||||
@@ -1,23 +1,13 @@
|
||||
// 获取客户端歌曲下载链接 - v1
|
||||
// 此版本不再采用 br 作为音质区分的标准
|
||||
// 而是采用 standard, exhigh, lossless, hires, jyeffect(高清臻音), vivid(臻音全景声), jymaster(超清母带), sky(沉浸环绕声) 进行音质判断
|
||||
// 而是采用 standard, exhigh, lossless, hires, jyeffect(高清环绕声), sky(沉浸环绕声), jymaster(超清母带) 进行音质判断
|
||||
|
||||
const createOption = require('../util/option.js')
|
||||
const { cookieToJson } = require('../util/index.js')
|
||||
module.exports = (query, request) => {
|
||||
const data = {
|
||||
id: query.id,
|
||||
immerseType: 'c51',
|
||||
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',
|
||||
}
|
||||
}
|
||||
return request(`/api/song/enhance/download/url/v1`, data, options)
|
||||
return request(`/api/song/enhance/download/url/v1`, data, createOption(query))
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
// 喜欢歌曲
|
||||
|
||||
const createOption = require('../util/option.js')
|
||||
module.exports = (query, request) => {
|
||||
const like = query.like !== 'false'
|
||||
const data = {
|
||||
trackId: query.id,
|
||||
userid: query.uid,
|
||||
like: like,
|
||||
}
|
||||
return request(`/api/song/like`, data, createOption(query))
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
// 插播相似歌曲
|
||||
|
||||
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'),
|
||||
)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user