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 |
@@ -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,24 +0,0 @@
|
||||
### CORS资源共享设置
|
||||
## 如果你需要配置, 请取消下面的注释并设置具体的域名
|
||||
# CORS_ALLOW_ORIGIN = "*"
|
||||
|
||||
### 代理设置
|
||||
## 启用反向代理
|
||||
ENABLE_PROXY = false
|
||||
## 代理配置
|
||||
PROXY_URL = "https://your-proxy-url.com/?proxy="
|
||||
|
||||
|
||||
### 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,132 +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报告前,请确认以下事项
|
||||
options:
|
||||
- label: 我已经搜索了现有的issues,确认这不是重复问题
|
||||
required: true
|
||||
- label: 我使用的是最新版本的API, 而且是官方发布的版本,而不是fork或修改版
|
||||
required: true
|
||||
- label: 不处理别人搭建的线上服务的问题,此项目提供任何线上服务不保证质量
|
||||
required: true
|
||||
- label: 如果不是提建议,提 issues 如果不照着模版来将不会优先处理或放着不管
|
||||
required: true
|
||||
- label: 维护项目都是业余时间,精力有限,我只能挑容易解决的issues处理,为了节约双方时间,请尽可能提供足够的有用的信息,给的信息不够我只能根据精力和时间看情况处理,如果模板信息看都不看就删掉,我不会进行任何回复,并且一个月后close掉issue
|
||||
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: os
|
||||
attributes:
|
||||
label: 操作系统或平台
|
||||
description: 您在哪个操作系统上遇到了这个问题?
|
||||
options:
|
||||
- Windows 10
|
||||
- Windows 11
|
||||
- Ubuntu 20.04
|
||||
- Ubuntu 22.04
|
||||
- macOS
|
||||
- 其他 Linux 发行版
|
||||
- 使用部署平台或其他 (请在描述中说明)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: deployment
|
||||
attributes:
|
||||
label: 部署平台
|
||||
description:
|
||||
options:
|
||||
- 我使用的自己的服务器部署
|
||||
- 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,5 +0,0 @@
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: 提问的艺术
|
||||
url: https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/main/README-zh_CN.md
|
||||
about: 默认所有 Issues 发起者均已了解此处的内容
|
||||
@@ -1,72 +0,0 @@
|
||||
name: ✨ 接口添加请求
|
||||
description: 希望添加新接口
|
||||
title: "[Feature] "
|
||||
labels: ["enhancement"]
|
||||
assignees: []
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
感谢您提出新功能建议!请填写以下信息帮助我们理解您的需求。
|
||||
|
||||
- type: checkboxes
|
||||
id: terms
|
||||
attributes:
|
||||
label: 确认事项
|
||||
description: 在提交功能请求前,请确认以下事项
|
||||
options:
|
||||
- label: 我已经搜索了现有的issues,确认这不是重复请求
|
||||
required: true
|
||||
- label: 我已经查看了项目文档
|
||||
required: true
|
||||
- label: 该项目符合该项目的目标和范围
|
||||
required: true
|
||||
- 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,55 +0,0 @@
|
||||
name: Publish Docker image
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
if: startsWith(github.event.release.tag_name, 'v')
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
# 读取 package.json 的版本号
|
||||
- name: Read package version
|
||||
id: pkg
|
||||
run: echo "VERSION=$(jq -r .version package.json)" >> $GITHUB_ENV
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: |
|
||||
moefurina/ncm-api:latest
|
||||
moefurina/ncm-api:${{ env.VERSION }}
|
||||
ghcr.io/neteasecloudmusicapienhanced/ncm-api:latest
|
||||
ghcr.io/neteasecloudmusicapienhanced/ncm-api:${{ env.VERSION }}
|
||||
platforms: linux/amd64,linux/arm64/v8
|
||||
@@ -1,109 +0,0 @@
|
||||
name: Build and Create PR
|
||||
|
||||
on:
|
||||
workflow_dispatch: # 手动触发
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
platform: linux
|
||||
target: node18-linux-x64
|
||||
output: precompiled/app
|
||||
- os: windows-latest
|
||||
platform: win
|
||||
target: node18-win-x64
|
||||
output: precompiled/app.exe
|
||||
- os: macos-latest
|
||||
platform: macos
|
||||
target: node18-macos-x64
|
||||
output: precompiled/app
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '18'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
npm install -g pnpm
|
||||
pnpm install
|
||||
|
||||
- name: Build for ${{ matrix.platform }}
|
||||
run: |
|
||||
npm run pkg${{ matrix.platform }}
|
||||
env:
|
||||
PKG_TARGET: ${{ matrix.target }}
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: app-${{ matrix.platform }}
|
||||
path: ${{ matrix.output }}
|
||||
if-no-files-found: error
|
||||
|
||||
create-pr:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'workflow_dispatch' # 只在手动触发时创建PR
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
path: precompiled
|
||||
|
||||
- name: Display structure of downloaded files
|
||||
run: ls -R
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
|
||||
- name: Create new branch and commit
|
||||
run: |
|
||||
git config --global user.name 'github-actions[bot]'
|
||||
git config --global user.email 'github-actions[bot]@users.noreply.github.com'
|
||||
|
||||
BRANCH_NAME="auto-build-$(date +%Y%m%d-%H%M%S)"
|
||||
echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_ENV
|
||||
git checkout -b $BRANCH_NAME
|
||||
|
||||
# 复制并整理下载的文件
|
||||
mkdir -p precompiled
|
||||
cp app-win/* precompiled/ || true
|
||||
cp app-linux/* precompiled/ || true
|
||||
cp app-macos/* precompiled/ || true
|
||||
|
||||
# 提交更改
|
||||
git add precompiled/
|
||||
git commit -m "Auto-build: Add compiled binaries for win, linux, macos" || exit 0
|
||||
|
||||
# 推送到远程仓库
|
||||
git push origin $BRANCH_NAME
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const { data: pullRequest } = await github.rest.pulls.create({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
title: 'Auto-build: Add compiled binaries for win, linux, macos',
|
||||
head: '${{ env.BRANCH_NAME }}',
|
||||
base: 'main',
|
||||
body: 'This PR contains newly built binaries for Windows, Linux, and macOS platforms.'
|
||||
});
|
||||
console.log(`Created PR #${pullRequest.number}: ${pullRequest.html_url}`);
|
||||
@@ -1,79 +0,0 @@
|
||||
name: Issue Management
|
||||
permissions:
|
||||
issues: write
|
||||
contents: read
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened, labeled]
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
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) 看是否有类似问题
|
||||
|
||||
handle-help-wanted:
|
||||
if: contains(github.event.issue.labels.*.name, 'question')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Add help resources
|
||||
uses: peter-evans/create-or-update-comment@v5
|
||||
with:
|
||||
issue-number: ${{ github.event.issue.number }}
|
||||
body: |
|
||||
🆘 需要帮助?
|
||||
|
||||
这里有一些可能对您有用的资源:
|
||||
|
||||
📚 **文档资源**
|
||||
- [项目文档](https://neteasecloudmusicapienhanced.js.org)
|
||||
|
||||
🔍 **常见问题**
|
||||
- 搜索[已关闭的issues](https://github.com/neteasecloudmusicapienhanced/api-enhanced/issues?q=is%3Aissue+is%3Aclosed) 看看是否有类似的问题已经被解答。
|
||||
|
||||
💬 **即时帮助**
|
||||
- 加入QQ群:https://qm.qq.com/q/TpeP9Uv2yk
|
||||
|
||||
如果以上资源无法解决您的问题,请提供更多详细信息,我们会尽快为您解答!
|
||||
|
||||
stale-issues:
|
||||
if: github.event.action == 'opened'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Mark stale issues
|
||||
uses: actions/stale@v10
|
||||
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,56 +0,0 @@
|
||||
name: Node.js CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
push:
|
||||
branches: [main]
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [16.x, 18.x, 22.x]
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
# Use built-in package manager cache for pnpm
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||
- name: Install pnpm
|
||||
run: npm install -g pnpm
|
||||
- name: Install dependencies (pnpm)
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Test
|
||||
env:
|
||||
NCM_API_TEST_LOGIN_COUNTRY_CODE: ${{ secrets.NCM_API_TEST_LOGIN_COUNTRY_CODE }}
|
||||
NCM_API_TEST_LOGIN_PHONE: ${{ secrets.NCM_API_TEST_LOGIN_PHONE }}
|
||||
NCM_API_TEST_LOGIN_PASSWORD: ${{ secrets.NCM_API_TEST_LOGIN_PASSWORD }}
|
||||
run: pnpm test
|
||||
lint:
|
||||
name: Lint
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [18.x]
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||
- name: Install pnpm
|
||||
run: npm install -g pnpm
|
||||
- name: Install dependencies (pnpm)
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Lint
|
||||
run: pnpm run lint
|
||||
@@ -1,39 +0,0 @@
|
||||
# .github/workflows/release.yml
|
||||
name: Release
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
release:
|
||||
if: startsWith(github.event.release.tag_name, 'v')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
# 发布到 NPM Registry
|
||||
- name: Setup Node.js for NPM
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 20
|
||||
registry-url: 'https://registry.npmjs.org/'
|
||||
- run: npm install
|
||||
- name: Publish to NPM
|
||||
run: npm publish --provenance --access public
|
||||
|
||||
# 发布到 GitHub Packages
|
||||
- name: Setup Node.js for GitHub Packages
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 20
|
||||
registry-url: 'https://npm.pkg.github.com'
|
||||
- name: Publish to GitHub Packages
|
||||
run: npm publish --access public
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ github.token }}
|
||||
@@ -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/[email protected]
|
||||
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,5 +8,3 @@ examples/moddef.json
|
||||
bin
|
||||
anonymous_token
|
||||
.vercel
|
||||
.env
|
||||
IFLOW.md
|
||||
@@ -1,4 +1,4 @@
|
||||
language: node_js
|
||||
|
||||
node_js:
|
||||
- 22
|
||||
- 12
|
||||
|
||||
@@ -1,32 +1,4 @@
|
||||
# 更新日志
|
||||
### 4.30.2 | 2026.02.22
|
||||
- feat(cloud): 添加云盘上传模式选择功能并优化上传配置 (#116)
|
||||
- refactor: Refactor random Chinese IP generator (#110)
|
||||
- fix(server): Correctly inject client IP into request parameters (#108)
|
||||
- refactor(ui): More elegant display of URL and debug buttons (#109)
|
||||
- feat: 添加评论统计数据 (#122)
|
||||
|
||||
### 4.30.1 | 2026.02.11
|
||||
- feat: Add user playlist endpoints & domain overrides (#105)
|
||||
|
||||
### 4.30.0 | 2026.02.06
|
||||
- feat: 新增音乐人黑胶会员任务接口 `/musician/vip/tasks` (#95)
|
||||
- feat: 自动构建: 添加Windows、Linux、macOS预编译二进制文件 (#88)
|
||||
- fix: 修复模块未定义问题
|
||||
- chore: 更新依赖项 (music-metadata: ^11.11.1 -> ^11.11.2, ansi-escapes: ^7.2.0 -> ^7.3.0, commander: ^14.0.2 -> ^14.0.3)
|
||||
- chore: 更新GitHub Actions (checkout: v4 -> v6, setup-node: v4 -> v6, upload-artifact: v4 -> v6, download-artifact: v4 -> v7, github-script: v7 -> v8)
|
||||
- refactor: 注释掉IP地址日志输出以提升隐私保护
|
||||
- refactor: 重构前端测试页面, 主要改进:
|
||||
- 统一使用简洁现代的设计风格
|
||||
- 去除渐变背景和复杂动画
|
||||
- 使用纯色背景(#f5f5f5)和白色卡片
|
||||
- 优化表单布局和交互体验
|
||||
- 增强错误处理和加载状态
|
||||
- 移除第三方框架依赖(Tailwind、Bootstrap、MDUI)
|
||||
- 升级Vue 2到Vue 3
|
||||
- 将硬编码的配置项移至前端表单
|
||||
- 所有页面现在都保持一致的设计语言,简洁清爽,功能完整。
|
||||
|
||||
### 4.25.0 | 2024.11.16
|
||||
- feat: 增加副歌时间、相关歌单推荐接口,原有相关歌单接口已废弃;fix: 将部分易盾白名单接口替换为eapi [#30](https://gitlab.com/Binaryify/neteasecloudmusicapi/-/merge_requests/30)
|
||||
- fix: 播客上传接口 [#32](https://gitlab.com/Binaryify/neteasecloudmusicapi/-/merge_requests/32)
|
||||
|
||||
@@ -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, 支持丰富的音乐相关接口,适合自建服务、二次开发和多平台部署(如果原版诈尸, 我会及时同步 or 归档)。
|
||||
[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 18 及以上
|
||||
- 推荐使用 pnpm 进行依赖管理
|
||||
|
||||
### 安装
|
||||
|
||||
```bash
|
||||
git clone https://github.com/neteasecloudmusicapienhanced/api-enhanced.git
|
||||
cd api
|
||||
pnpm i
|
||||
```shell
|
||||
$ git clone [email protected]: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,151 +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_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等加密算法所做的贡献
|
||||
|
||||
项目参考:
|
||||
|
||||
[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)
|
||||
|
||||
### 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)
|
||||

|
||||
|
||||
## License
|
||||
|
||||
[MIT License](https://github.com/MoeFurina/NeteaseCloudMusicApiEnhanced/blob/main/LICENSE)
|
||||
[The MIT License (MIT)](https://gitlab.com/Binaryify/NeteaseCloudMusicApi/blob/main/LICENSE)
|
||||
|
||||
@@ -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,7 +2,6 @@ export interface RequestBaseConfig {
|
||||
cookie?: string
|
||||
realIP?: string // IPv4/IPv6 filled in X-Real-IP
|
||||
proxy?: string // HTTP proxy
|
||||
randomCNIP?: boolean // Whether to use a random Chinese IP address filled in X-Real-IP
|
||||
}
|
||||
|
||||
export interface MultiPageConfig {
|
||||
@@ -1166,13 +1165,6 @@ export function song_url_v1(
|
||||
params: { id: string | number; level: SoundQualityType } & RequestBaseConfig,
|
||||
): Promise<Response>
|
||||
|
||||
export function song_copyright_rcmd(
|
||||
params: {
|
||||
songid?: string | number
|
||||
id?: string | number
|
||||
} & RequestBaseConfig,
|
||||
): Promise<Response>
|
||||
|
||||
export function top_album(
|
||||
params: {
|
||||
area?: AlbumListArea
|
||||
@@ -1722,8 +1714,6 @@ export function nickname_check(
|
||||
|
||||
export function musician_tasks_new(params: RequestBaseConfig): Promise<Response>
|
||||
|
||||
export function musician_vip_tasks(params: RequestBaseConfig): Promise<Response>
|
||||
|
||||
export function playlist_update_playcount(
|
||||
params: {
|
||||
id?: number | string
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
## 环境
|
||||
- 系统/平台: <你的系统和平台>
|
||||
|
||||
- nodejs 版本: <你的 NodeJS 版本号>
|
||||
|
||||
- API版本:<运行的云音乐 API 的版本号, 对应 package.json 里面的 version>
|
||||
|
||||
## 出现问题
|
||||
<出现的问题>
|
||||
|
||||
## 重现步骤
|
||||
<重现步骤>
|
||||
|
||||
## 期待效果
|
||||
<现在的效果,期待的效果>
|
||||
|
||||
|
||||
|
||||
>先看文档有没有相关说明,调用前须知必看
|
||||
|
||||
>先在 issues 搜一下是否有相似问题,没有再发,否则直接关闭
|
||||
|
||||
>不处理别人搭建的线上服务的问题,此项目不提供任何线上服务,请自行搭建
|
||||
|
||||
>重现步骤尽量详细,不能含糊不清,包含请求地址和对应参数以及操作过程描述,不是每个人都喜欢猜别人遇到了什么问题和找参数一个个试,也比较浪费时间
|
||||
|
||||
>如果不是提建议,提 issues 如果不照着模版来将不会优先处理或直接关闭
|
||||
|
||||
>460 cheating 的问题把 `util/request.js` 里面的 `headers['X-Real-IP']` 的注释取消掉就好
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
// 获取指定维度音乐排行榜详情
|
||||
|
||||
const createOption = require('../util/option.js')
|
||||
module.exports = (query, request) => {
|
||||
const data = {
|
||||
chartCode: query.chartCode,
|
||||
targetId: query.targetId,
|
||||
targetType: query.targetType,
|
||||
}
|
||||
return request(`/api/chart/detail`, data, createOption(query))
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// 获取指定维度音乐排行榜列表
|
||||
|
||||
const createOption = require('../util/option.js')
|
||||
module.exports = (query, request) => {
|
||||
const data = {
|
||||
chartCode: query.chartCode,
|
||||
targetId: query.targetId,
|
||||
targetType: query.targetType,
|
||||
}
|
||||
return request(`/api/chart/song/detail`, data, createOption(query))
|
||||
}
|
||||
@@ -1,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,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,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))
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// 多级行政区划数据获取接口
|
||||
|
||||
const createOption = require('../util/option.js')
|
||||
module.exports = (query, request) => {
|
||||
const data = {
|
||||
bizCode: query.bizCode || '',
|
||||
}
|
||||
return request(`/api/lbs/city/code`, data, createOption(query))
|
||||
}
|
||||
@@ -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,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'))
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -2,7 +2,6 @@ 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')
|
||||
@@ -24,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(
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
}
|
||||
@@ -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,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,38 +0,0 @@
|
||||
// 网易云歌曲解灰(适配SPlayer的UNM-Server)
|
||||
// 支持qq音乐、酷狗音乐、酷我音乐、咪咕音乐、第三方网易云API等等(来自GD音乐台)
|
||||
|
||||
const createOption = require('../util/option.js')
|
||||
const logger = require('../util/logger.js')
|
||||
|
||||
module.exports = async (query, request) => {
|
||||
try {
|
||||
const {
|
||||
matchID,
|
||||
} = require('@neteasecloudmusicapienhanced/unblockmusic-utils')
|
||||
const result = await matchID(query.id, query.source)
|
||||
const proxy = process.env.PROXY_URL
|
||||
logger.info('开始解灰', query.id, result)
|
||||
const useProxy = process.env.ENABLE_PROXY || 'false'
|
||||
if (result.data.url && result.data.url.includes('kuwo')) {
|
||||
result.proxyUrl =
|
||||
useProxy === 'true' ? proxy + result.data.url : result.data.url
|
||||
}
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
code: 200,
|
||||
data: result.data.url,
|
||||
proxyUrl: result.proxyUrl || '',
|
||||
},
|
||||
}
|
||||
} catch (e) {
|
||||
return {
|
||||
status: 500,
|
||||
body: {
|
||||
code: 500,
|
||||
msg: e.message || 'unblock error',
|
||||
data: [],
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
// 夹带私货的东西就不要放在这里了
|
||||
|
||||
module.exports = async (query, request) => {
|
||||
return { status: 200, body: { code: 200, data: [] } }
|
||||
}
|
||||
@@ -1,55 +1,14 @@
|
||||
// 歌曲链接 - v1
|
||||
// 此版本不再采用 br 作为音质区分的标准
|
||||
// 而是采用 standard, exhigh, lossless, hires, jyeffect(高清环绕声), sky(沉浸环绕声), jymaster(超清母带) 进行音质判断
|
||||
// 当unblock为true时, 会尝试使用unblockmusic-utils进行解锁, 同时音质设置不会生效, 但仍然为必须传入参数
|
||||
|
||||
const logger = require('../util/logger.js')
|
||||
const createOption = require('../util/option.js')
|
||||
module.exports = async (query, request) => {
|
||||
const {
|
||||
matchID,
|
||||
} = require('@neteasecloudmusicapienhanced/unblockmusic-utils')
|
||||
require('dotenv').config()
|
||||
module.exports = (query, request) => {
|
||||
const data = {
|
||||
ids: '[' + query.id + ']',
|
||||
level: query.level,
|
||||
encodeType: 'flac',
|
||||
}
|
||||
if (query.unblock === 'true') {
|
||||
try {
|
||||
const result = await matchID(query.id, query.source)
|
||||
logger.info('Starting unblock(uses modules unblock):', query.id, result)
|
||||
const useProxy = process.env.ENABLE_PROXY || 'false'
|
||||
let proxyUrl = ''
|
||||
if (result.data.url && result.data.url.includes('kuwo')) {
|
||||
proxyUrl =
|
||||
useProxy === 'true' && process.env.PROXY_URL
|
||||
? process.env.PROXY_URL + result.data.url
|
||||
: result.data.url
|
||||
}
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
code: 200,
|
||||
msg: 'Warning: Customizing unblock sources is not supported on this endpoint. Please use `/song/url/match` instead.',
|
||||
data: [
|
||||
{
|
||||
id: Number(query.id),
|
||||
url: result.data.url,
|
||||
type: 'flac',
|
||||
level: query.level,
|
||||
freeTrialInfo: 'null',
|
||||
fee: 0,
|
||||
proxyUrl: proxyUrl || '',
|
||||
},
|
||||
],
|
||||
},
|
||||
cookie: [],
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error in unblocking music:', e)
|
||||
}
|
||||
}
|
||||
if (data.level == 'sky') {
|
||||
data.immerseType = 'c51'
|
||||
}
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
// 获取客户端歌曲下载链接 - v1
|
||||
// 此版本不再采用 br 作为音质区分的标准
|
||||
// 而是采用 standard, exhigh, lossless, hires, jyeffect(高清环绕声), sky(沉浸环绕声), jymaster(超清母带) 进行音质判断
|
||||
|
||||
const createOption = require('../util/option.js')
|
||||
module.exports = async (query, request) => {
|
||||
const data = {
|
||||
id: query.id,
|
||||
immerseType: 'c51',
|
||||
level: query.level,
|
||||
}
|
||||
const response = await request(
|
||||
`/api/song/enhance/download/url/v1`,
|
||||
data,
|
||||
createOption(query),
|
||||
)
|
||||
let url = response?.body?.data?.[0]?.url
|
||||
|
||||
if (!url) {
|
||||
const fallbackData = {
|
||||
ids: `[${query.id}]`,
|
||||
level: query.level,
|
||||
encodeType: 'flac',
|
||||
}
|
||||
if (query.level === 'sky') {
|
||||
fallbackData.immerseType = 'c51'
|
||||
}
|
||||
const fallback = await request(
|
||||
`/api/song/enhance/player/url/v1`,
|
||||
fallbackData,
|
||||
createOption(query),
|
||||
)
|
||||
url = fallback?.body?.data?.[0]?.url
|
||||
|
||||
if (!url) {
|
||||
return fallback
|
||||
}
|
||||
|
||||
return {
|
||||
status: 302,
|
||||
body: '',
|
||||
cookie: fallback.cookie || [],
|
||||
redirectUrl: url,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
status: 302,
|
||||
body: '',
|
||||
cookie: response.cookie || [],
|
||||
redirectUrl: url,
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ module.exports = (query, request) => {
|
||||
const data = {
|
||||
userId: query.uid,
|
||||
time: '0',
|
||||
limit: query.limit || 20,
|
||||
limit: query.limit || 30,
|
||||
offset: query.offset || 0,
|
||||
getcounts: 'true',
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
// 黑胶乐签打卡
|
||||
|
||||
const createOption = require('../util/option.js')
|
||||
module.exports = (query, request) => {
|
||||
const data = {}
|
||||
return request(`/api/vip-center-bff/task/sign`, data, createOption(query))
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
// 黑胶乐签签到信息
|
||||
|
||||
const createOption = require('../util/option.js')
|
||||
module.exports = (query, request) => {
|
||||
const data = {}
|
||||
return request(
|
||||
`/api/vipnewcenter/app/user/sign/info`,
|
||||
data,
|
||||
createOption(query, 'weapi'),
|
||||
)
|
||||
}
|
||||
@@ -1,26 +1,25 @@
|
||||
const { default: axios } = require('axios')
|
||||
const fs = require('fs')
|
||||
var xml2js = require('xml2js')
|
||||
|
||||
const createOption = require('../util/option.js')
|
||||
const { getFileExtension, readFileChunk } = require('../util/fileHelper')
|
||||
|
||||
var parser = new xml2js.Parser()
|
||||
|
||||
var parser = new xml2js.Parser(/* options */)
|
||||
function createDupkey() {
|
||||
// 格式:3b443c7c-a87f-468d-ba38-46d407aaf23a
|
||||
var s = []
|
||||
var hexDigits = '0123456789abcdef'
|
||||
for (var i = 0; i < 36; i++) {
|
||||
s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1)
|
||||
}
|
||||
s[14] = '4'
|
||||
s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1)
|
||||
s[14] = '4' // bits 12-15 of the time_hi_and_version field to 0010
|
||||
s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1) // bits 6-7 of the clock_seq_hi_and_reserved to 01
|
||||
s[8] = s[13] = s[18] = s[23] = '-'
|
||||
return s.join('')
|
||||
}
|
||||
|
||||
module.exports = async (query, request) => {
|
||||
const ext = getFileExtension(query.songFile.name)
|
||||
let ext = 'mp3'
|
||||
if (query.songFile.name.indexOf('flac') > -1) {
|
||||
ext = 'flac'
|
||||
}
|
||||
const filename =
|
||||
query.songName ||
|
||||
query.songFile.name
|
||||
@@ -51,58 +50,43 @@ module.exports = async (query, request) => {
|
||||
createOption(query, 'weapi'),
|
||||
)
|
||||
|
||||
const objectKey = tokenRes.body.result.objectKey.replace(/\//g, '%2F')
|
||||
const objectKey = tokenRes.body.result.objectKey.replace('/', '%2F')
|
||||
const docId = tokenRes.body.result.docId
|
||||
const res = await axios({
|
||||
method: 'post',
|
||||
url: `https://ymusic.nos-hz.163yun.com/${objectKey}?uploads`,
|
||||
headers: {
|
||||
'x-nos-token': tokenRes.body.result.token,
|
||||
'X-Nos-Meta-Content-Type': query.songFile.mimetype || 'audio/mpeg',
|
||||
'X-Nos-Meta-Content-Type': 'audio/mpeg',
|
||||
},
|
||||
data: null,
|
||||
})
|
||||
|
||||
// return xml
|
||||
const res2 = await parser.parseStringPromise(res.data)
|
||||
|
||||
const useTempFile = !!query.songFile.tempFilePath
|
||||
let fileSize = query.songFile.size
|
||||
|
||||
if (useTempFile) {
|
||||
const stats = await fs.promises.stat(query.songFile.tempFilePath)
|
||||
fileSize = stats.size
|
||||
}
|
||||
|
||||
const blockSize = 10 * 1024 * 1024
|
||||
const fileSize = query.songFile.data.length
|
||||
const blockSize = 10 * 1024 * 1024 // 10MB
|
||||
let offset = 0
|
||||
let blockIndex = 1
|
||||
|
||||
let etags = []
|
||||
|
||||
while (offset < fileSize) {
|
||||
let chunk
|
||||
if (useTempFile) {
|
||||
chunk = await readFileChunk(
|
||||
query.songFile.tempFilePath,
|
||||
offset,
|
||||
Math.min(blockSize, fileSize - offset),
|
||||
)
|
||||
} else {
|
||||
chunk = query.songFile.data.slice(
|
||||
offset,
|
||||
Math.min(offset + blockSize, fileSize),
|
||||
)
|
||||
}
|
||||
const chunk = query.songFile.data.slice(
|
||||
offset,
|
||||
Math.min(offset + blockSize, fileSize),
|
||||
)
|
||||
|
||||
const res3 = await axios({
|
||||
method: 'put',
|
||||
url: `https://ymusic.nos-hz.163yun.com/${objectKey}?partNumber=${blockIndex}&uploadId=${res2.InitiateMultipartUploadResult.UploadId[0]}`,
|
||||
headers: {
|
||||
'x-nos-token': tokenRes.body.result.token,
|
||||
'Content-Type': query.songFile.mimetype || 'audio/mpeg',
|
||||
'Content-Type': 'audio/mpeg',
|
||||
},
|
||||
data: chunk,
|
||||
})
|
||||
// get etag
|
||||
const etag = res3.headers.etag
|
||||
etags.push(etag)
|
||||
offset += blockSize
|
||||
@@ -117,17 +101,19 @@ module.exports = async (query, request) => {
|
||||
}
|
||||
completeStr += '</CompleteMultipartUpload>'
|
||||
|
||||
// 文件处理
|
||||
await axios({
|
||||
method: 'post',
|
||||
url: `https://ymusic.nos-hz.163yun.com/${objectKey}?uploadId=${res2.InitiateMultipartUploadResult.UploadId[0]}`,
|
||||
headers: {
|
||||
'Content-Type': 'text/plain;charset=UTF-8',
|
||||
'X-Nos-Meta-Content-Type': query.songFile.mimetype || 'audio/mpeg',
|
||||
'X-Nos-Meta-Content-Type': 'audio/mpeg',
|
||||
'x-nos-token': tokenRes.body.result.token,
|
||||
},
|
||||
data: completeStr,
|
||||
})
|
||||
|
||||
// preCheck
|
||||
await request(
|
||||
`/api/voice/workbench/voice/batch/upload/preCheck`,
|
||||
{
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
// 我创建的播客声音
|
||||
|
||||
const createOption = require('../util/option.js')
|
||||
module.exports = (query, request) => {
|
||||
const data = {
|
||||
limit: query.limit || 20,
|
||||
}
|
||||
return request(
|
||||
`/api/social/my/created/voicelist/v1`,
|
||||
data,
|
||||
createOption(query, 'weapi'),
|
||||
)
|
||||
}
|
||||
@@ -1,11 +1,14 @@
|
||||
const createOption = require('../util/option.js')
|
||||
module.exports = (query, request) => {
|
||||
const data = {
|
||||
keyword: query.keyword || '',
|
||||
scene: 'normal',
|
||||
limit: query.limit || '10',
|
||||
offset: query.offset || '30',
|
||||
e_r: true,
|
||||
fee: '-1',
|
||||
limit: query.limit || '200',
|
||||
offset: query.offset || '0',
|
||||
podcastName: query.podcastName || '',
|
||||
}
|
||||
return request(`/api/search/voicelist/get`, data, createOption(query))
|
||||
return request(
|
||||
`/api/voice/workbench/voicelist/search`,
|
||||
data,
|
||||
createOption(query),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
const { eapiResDecrypt, eapiReqDecrypt } = require('../util/crypto')
|
||||
const logger = require('../util/logger.js')
|
||||
|
||||
let reqParamsHex =
|
||||
'6E0B1C712DCB3648589D7C950C296204072A88C3E080C4CFFD0A71A553EC2533BA88E11B1E1C6DF3BE8EFA26177FCB6FCA34EB3FAFAB4671B2BBAFA9781AFDA2BF53A3DC423722493837B9BC6E80CED5BBD2DDC2856920E4D4E3E7F3EB77ECF265217A66AE677BE36F2D6FB203F721CA250E1453EA61A34904E33D5FCB9D483601D744BE0AE979AC911A00F25828538844F4B1C24F6C34880A4AB257F530C7FB10A81FED32B18D09D70C0B1B9D34A2E58A3C3FAD382C6F958077059C4F801AD7B3B248FDB9D7A59B6A9EEFF8C781A84315B33A7AFD48BE9FCFCBE1902CCC27949ACF2BDE3FA34D116E230C3597E8320B8C42BBBF371A00C03EC428E0440EB94C1540F3FD4173D29E310AFE43AB0EF449852904103EF305FC435AD43B7D8673642F74C89CCB2F1A6A79B3BE14F1235D3843C3B241D12C05DBDDF37B68CA8B5D0230AF1FCF2A9705886F4D126B33FFF6948DE1E4046DB6423D687E96C5B65122464D2E71AEC7722935FF2C3796FAE253A16AA3B102FBE7296AB0DB9EA5C46AD12B'.replaceAll(
|
||||
@@ -11,6 +10,6 @@ const resHex =
|
||||
' ',
|
||||
'',
|
||||
)
|
||||
logger.info(reqParamsHex)
|
||||
logger.info(eapiReqDecrypt(reqParamsHex))
|
||||
logger.info(eapiResDecrypt(resHex))
|
||||
console.log(reqParamsHex)
|
||||
console.log(eapiReqDecrypt(reqParamsHex))
|
||||
console.log(eapiResDecrypt(resHex))
|
||||
|
||||
@@ -2,7 +2,6 @@ const { cloud, login_cellphone } = require('../main')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const yargs = require('yargs')
|
||||
const logger = require('../util/logger.js')
|
||||
|
||||
const MUSIC_FILE_EXTENSIONS = new Set(['.mp3', '.flac'])
|
||||
|
||||
@@ -38,11 +37,11 @@ async function uploadArrayOfFile(token, arrayOfFiles) {
|
||||
cookie: token.body.cookie,
|
||||
})
|
||||
} catch (error) {
|
||||
logger.info(error)
|
||||
console.log(error)
|
||||
failed += 1
|
||||
failedFiles.push(file)
|
||||
}
|
||||
logger.info(`Uploaded ${k + 1}/${fileCount} songs`)
|
||||
console.log(`Uploaded ${k + 1}/${fileCount} songs`)
|
||||
}
|
||||
return { failedFiles, failed }
|
||||
}
|
||||
@@ -87,19 +86,19 @@ async function main() {
|
||||
const files = args.file ? [args.file] : getAllMusicFiles(args.dir)
|
||||
const fileCount = files.length
|
||||
|
||||
logger.info(`Found ${fileCount} files, uploading...`)
|
||||
console.log(`Found ${fileCount} files, uploading...`)
|
||||
let res = await uploadArrayOfFile(token, files)
|
||||
if (res.failed) {
|
||||
logger.info(`Failed to upload ${res.failed} songs, retrying...`)
|
||||
console.log(`Failed to upload ${res.failed} songs, retrying...`)
|
||||
res = await uploadArrayOfFile(token, res.failedFiles)
|
||||
}
|
||||
|
||||
logger.info(`Uploaded ${fileCount - res.failed} songs`)
|
||||
logger.info(
|
||||
console.log(`Uploaded ${fileCount - res.failed} songs`)
|
||||
console.log(
|
||||
`Failed to upload ${res.failed} songs, you can reupload the files below`,
|
||||
)
|
||||
for (let k in res.failedFiles) {
|
||||
logger.info(res.failedFiles[k])
|
||||
console.log(res.failedFiles[k])
|
||||
}
|
||||
}
|
||||
main()
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
const { cloud, login_cellphone } = require('../main')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const logger = require('../util/logger.js')
|
||||
|
||||
async function main() {
|
||||
const result = await login_cellphone({
|
||||
@@ -18,7 +17,7 @@ async function main() {
|
||||
cookie: result.body.cookie,
|
||||
})
|
||||
} catch (error) {
|
||||
logger.info(error, 'error')
|
||||
console.log(error, 'error')
|
||||
}
|
||||
}
|
||||
main()
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
const logger = require('../util/logger.js')
|
||||
const {
|
||||
login_cellphone,
|
||||
user_cloud,
|
||||
@@ -11,22 +10,22 @@ async function test() {
|
||||
phone: '手机号',
|
||||
password: '密码',
|
||||
})
|
||||
logger.info(result)
|
||||
console.log(result)
|
||||
const result2 = await user_cloud({
|
||||
cookie: result.body.cookie,
|
||||
})
|
||||
logger.info(result2.body)
|
||||
console.log(result2.body)
|
||||
const result3 = await album_sublist({
|
||||
cookie: result.body.cookie,
|
||||
})
|
||||
logger.info(result3.body)
|
||||
console.log(result3.body)
|
||||
const result4 = await song_url({
|
||||
cookie: result.body.cookie,
|
||||
id: 33894312,
|
||||
})
|
||||
logger.info(result4.body)
|
||||
console.log(result4.body)
|
||||
} catch (error) {
|
||||
logger.info(error)
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
test()
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { banner, lyric } from '@neteasecloudmusicapienhanced/api'
|
||||
import { logger } from '../util/logger.js'
|
||||
import { banner, lyric } from 'NeteaseCloudMusicApi'
|
||||
banner({ type: 0 }).then((res) => {
|
||||
logger.info(res)
|
||||
console.log(res)
|
||||
})
|
||||
lyric({
|
||||
id: '33894312',
|
||||
}).then((res) => {
|
||||
logger.info(res)
|
||||
console.log(res)
|
||||
})
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
{
|
||||
"name": "@neteasecloudmusicapienhanced/api",
|
||||
"version": "4.32.1",
|
||||
"description": "全网最全的网易云音乐API接口 || A revival project for NeteaseCloudMusicApi Node.js Services (Half Refactor & Enhanced) || 网易云音乐 API 备份 + 增强 || 本项目自原版v4.28.0版本后开始自行维护",
|
||||
"name": "NeteaseCloudMusicApi",
|
||||
"version": "4.32.0",
|
||||
"description": "网易云音乐 NodeJS 版 API",
|
||||
"scripts": {
|
||||
"dev": "nodemon app.js",
|
||||
"start": "node app.js",
|
||||
"test": "mocha -r intelli-espower-loader -t 60000 server.test.js main.test.js --exit",
|
||||
"lint": "eslint \"**/*.{js,ts}\"",
|
||||
"lint-fix": "eslint --fix \"**/*.{js,ts}\"",
|
||||
"prepare": "husky install",
|
||||
"pkgwin": "pkg . -t node18-win-x64 -C GZip -o precompiled/app",
|
||||
"pkglinux": "pkg . -t node18-linux-x64 -C GZip -o precompiled/app",
|
||||
"pkgmacos": "pkg . -t node18-macos-x64 -C GZip -o precompiled/app"
|
||||
"pkgwin": "pkg . -t node18-win-x64 -C GZip -o bin/app --no-bytecode",
|
||||
"pkglinux": "pkg . -t node18-linux-x64 -C GZip -o bin/app --no-bytecode",
|
||||
"pkgmacos": "pkg . -t node18-macos-x64 -C GZip -o bin/app --no-bytecode"
|
||||
},
|
||||
"bin": "./app.js",
|
||||
"pkg": {
|
||||
@@ -20,6 +19,7 @@
|
||||
"node_modules/axios",
|
||||
"node_modules/express",
|
||||
"node_modules/express-fileupload",
|
||||
"node_modules/md5",
|
||||
"node_modules/music-metadata",
|
||||
"node_modules/pac-proxy-agent",
|
||||
"node_modules/qrcode",
|
||||
@@ -50,7 +50,7 @@
|
||||
"git add"
|
||||
]
|
||||
},
|
||||
"author": "MoeFurina",
|
||||
"author": "binaryify",
|
||||
"license": "MIT",
|
||||
"files": [
|
||||
"module",
|
||||
@@ -65,44 +65,38 @@
|
||||
"data"
|
||||
],
|
||||
"dependencies": {
|
||||
"@neteasecloudmusicapienhanced/unblockmusic-utils": "^0.3.1",
|
||||
"axios": "^1.16.1",
|
||||
"axios": "^1.13.6",
|
||||
"crypto-js": "^4.2.0",
|
||||
"dotenv": "^17.4.2",
|
||||
"express": "^5.2.1",
|
||||
"express": "^4.22.1",
|
||||
"express-fileupload": "^1.5.2",
|
||||
"gzip": "^0.1.0",
|
||||
"music-metadata": "^11.12.3",
|
||||
"node-forge": "^1.4.0",
|
||||
"md5": "^2.3.0",
|
||||
"music-metadata": "^7.14.0",
|
||||
"node-forge": "^1.3.3",
|
||||
"pac-proxy-agent": "^7.2.0",
|
||||
"qrcode": "^1.5.4",
|
||||
"safe-decode-uri-component": "^1.2.1",
|
||||
"tunnel": "^0.0.6",
|
||||
"xml2js": "^0.6.2",
|
||||
"yargs": "^18.0.0"
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.3.5",
|
||||
"@eslint/js": "^9.39.4",
|
||||
"@types/express": "^5.0.6",
|
||||
"@types/express": "^4.17.25",
|
||||
"@types/express-fileupload": "^1.5.1",
|
||||
"@types/mocha": "^10.0.10",
|
||||
"@types/node": "25.5.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.59.3",
|
||||
"@typescript-eslint/parser": "^8.59.3",
|
||||
"eslint": "^9.39.4",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-html": "^8.1.4",
|
||||
"eslint-plugin-prettier": "^5.5.5",
|
||||
"globals": "^17.6.0",
|
||||
"husky": "^9.1.7",
|
||||
"intelli-espower-loader": "^1.1.0",
|
||||
"lint-staged": "^16.4.0",
|
||||
"mocha": "^11.7.5",
|
||||
"nodemon": "^3.1.14",
|
||||
"@types/mocha": "^9.1.1",
|
||||
"@types/node": "16.11.19",
|
||||
"@typescript-eslint/eslint-plugin": "5.0.0",
|
||||
"@typescript-eslint/parser": "5.0.0",
|
||||
"eslint": "8.7.0",
|
||||
"eslint-config-prettier": "8.5.0",
|
||||
"eslint-plugin-html": "7.1.0",
|
||||
"eslint-plugin-prettier": "4.0.0",
|
||||
"husky": "7.0.4",
|
||||
"intelli-espower-loader": "1.1.0",
|
||||
"lint-staged": "12.1.7",
|
||||
"mocha": "10.0.0",
|
||||
"pkg": "^5.8.1",
|
||||
"power-assert": "^1.6.1",
|
||||
"prettier": "^3.8.3",
|
||||
"typescript": "^5.9.3"
|
||||
"power-assert": "1.6.1",
|
||||
"prettier": "2.7.1",
|
||||
"typescript": "4.5.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
const { default: axios } = require('axios')
|
||||
const createOption = require('../util/option.js')
|
||||
const logger = require('../util/logger.js')
|
||||
const {
|
||||
getUploadData,
|
||||
getFileExtension,
|
||||
sanitizeFilename,
|
||||
} = require('../util/fileHelper')
|
||||
|
||||
module.exports = async (query, request) => {
|
||||
const ext = getFileExtension(query.songFile.name)
|
||||
const filename = sanitizeFilename(query.songFile.name)
|
||||
let ext = 'mp3'
|
||||
// if (query.songFile.name.indexOf('flac') > -1) {
|
||||
// ext = 'flac'
|
||||
// }
|
||||
if (query.songFile.name.includes('.')) {
|
||||
ext = query.songFile.name.split('.').pop()
|
||||
}
|
||||
const filename = query.songFile.name
|
||||
.replace('.' + ext, '')
|
||||
.replace(/\s/g, '')
|
||||
.replace(/\./g, '_')
|
||||
const bucket = 'jd-musicrep-privatecloud-audio-public'
|
||||
|
||||
// 获取key和token
|
||||
const tokenRes = await request(
|
||||
`/api/nos/token/alloc`,
|
||||
{
|
||||
@@ -26,82 +28,31 @@ module.exports = async (query, request) => {
|
||||
createOption(query, 'weapi'),
|
||||
)
|
||||
|
||||
if (!tokenRes.body.result || !tokenRes.body.result.objectKey) {
|
||||
logger.error('Token分配失败:', tokenRes.body)
|
||||
throw {
|
||||
status: 500,
|
||||
body: {
|
||||
code: 500,
|
||||
msg: '获取上传token失败',
|
||||
detail: tokenRes.body,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const objectKey = tokenRes.body.result.objectKey.replace(/\//g, '%2F')
|
||||
let lbs
|
||||
// 上传
|
||||
const objectKey = tokenRes.body.result.objectKey.replace('/', '%2F')
|
||||
try {
|
||||
lbs = (
|
||||
const lbs = (
|
||||
await axios({
|
||||
method: 'get',
|
||||
url: `https://wanproxy.127.net/lbs?version=1.0&bucketname=${bucket}`,
|
||||
timeout: 10000,
|
||||
})
|
||||
).data
|
||||
} catch (error) {
|
||||
logger.error('LBS获取失败:', error.message)
|
||||
throw {
|
||||
status: 500,
|
||||
body: {
|
||||
code: 500,
|
||||
msg: '获取上传服务器地址失败',
|
||||
detail: error.message,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if (!lbs || !lbs.upload || !lbs.upload[0]) {
|
||||
logger.error('无效的LBS响应:', lbs)
|
||||
throw {
|
||||
status: 500,
|
||||
body: {
|
||||
code: 500,
|
||||
msg: '获取上传服务器地址无效',
|
||||
detail: lbs,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await axios({
|
||||
method: 'post',
|
||||
url: `${lbs.upload[0]}/${bucket}/${objectKey}?offset=0&complete=true&version=1.0`,
|
||||
headers: {
|
||||
'x-nos-token': tokenRes.body.result.token,
|
||||
'Content-MD5': query.songFile.md5,
|
||||
'Content-Type': query.songFile.mimetype || 'audio/mpeg',
|
||||
'Content-Type': 'audio/mpeg',
|
||||
'Content-Length': String(query.songFile.size),
|
||||
},
|
||||
data: getUploadData(query.songFile),
|
||||
data: query.songFile.data,
|
||||
maxContentLength: Infinity,
|
||||
maxBodyLength: Infinity,
|
||||
timeout: 300000,
|
||||
})
|
||||
logger.info('上传成功:', filename)
|
||||
} catch (error) {
|
||||
logger.error('上传失败:', {
|
||||
status: error.response?.status,
|
||||
data: error.response?.data,
|
||||
message: error.message,
|
||||
})
|
||||
throw {
|
||||
status: error.response?.status || 500,
|
||||
body: {
|
||||
code: error.response?.status || 500,
|
||||
msg: '文件上传失败',
|
||||
detail: error.response?.data || error.message,
|
||||
},
|
||||
}
|
||||
console.log('error', error.response)
|
||||
throw error.response
|
||||
}
|
||||
return {
|
||||
...tokenRes,
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
const { default: axios } = require('axios')
|
||||
const createOption = require('../util/option.js')
|
||||
const { getUploadData } = require('../util/fileHelper')
|
||||
|
||||
module.exports = async (query, request) => {
|
||||
const data = {
|
||||
bucket: 'yyimgs',
|
||||
@@ -12,23 +10,27 @@ module.exports = async (query, request) => {
|
||||
return_body: `{"code":200,"size":"$(ObjectSize)"}`,
|
||||
type: 'other',
|
||||
}
|
||||
// 获取key和token
|
||||
const res = await request(
|
||||
`/api/nos/token/alloc`,
|
||||
data,
|
||||
createOption(query, 'weapi'),
|
||||
)
|
||||
|
||||
// 上传图片
|
||||
const res2 = await axios({
|
||||
method: 'post',
|
||||
url: `https://nosup-hz1.127.net/yyimgs/${res.body.result.objectKey}?offset=0&complete=true&version=1.0`,
|
||||
headers: {
|
||||
'x-nos-token': res.body.result.token,
|
||||
'Content-Type': query.imgFile.mimetype || 'image/jpeg',
|
||||
'Content-Type': 'image/jpeg',
|
||||
},
|
||||
data: getUploadData(query.imgFile),
|
||||
data: query.imgFile.data,
|
||||
})
|
||||
|
||||
return {
|
||||
// ...res.body.result,
|
||||
// ...res2.data,
|
||||
// ...res3.body,
|
||||
url_pre: 'https://p1.music.126.net/' + res.body.result.objectKey,
|
||||
imgId: res.body.result.docId,
|
||||
}
|
||||
|
||||
@@ -5,148 +5,82 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>API 调试界面</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
padding: 20px;
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 32px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
input, button {
|
||||
padding: 10px;
|
||||
box-sizing: border-box;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #555;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
input, select {
|
||||
padding: 10px 14px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
flex: 1;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
input:focus, select:focus {
|
||||
border-color: #333;
|
||||
}
|
||||
|
||||
button {
|
||||
background: #333;
|
||||
color: white;
|
||||
padding: 10px 24px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
background-color: #4CAF50;
|
||||
color: white;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
.data-result {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
min-height: 400px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.data-result > div {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
padding: 10px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.data-result label {
|
||||
margin-bottom: 8px;
|
||||
padding: 0;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
textarea {
|
||||
flex: 1;
|
||||
padding: 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
resize: vertical;
|
||||
min-height: 350px;
|
||||
outline: none;
|
||||
#data, #result {
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
textarea:focus {
|
||||
border-color: #333;
|
||||
#data {
|
||||
border-right: 1px solid #ccc;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>API 调试界面</h1>
|
||||
<form onsubmit="event.preventDefault(); sendRequest();">
|
||||
<div class="form-row">
|
||||
<label for="uri">URI</label>
|
||||
<input type="text" id="uri" name="uri" value="/api/song/lyric/v1">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="crypto">加密方式</label>
|
||||
<select id="crypto" name="crypto">
|
||||
<option value="weapi">weapi</option>
|
||||
<option value="eapi">eapi</option>
|
||||
<option value="api">api</option>
|
||||
<option value="linuxapi">linuxapi</option>
|
||||
<option value="" selected>(默认)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label></label>
|
||||
<button type="submit">发送请求</button>
|
||||
</div>
|
||||
<label for="uri">uri</label>
|
||||
<input type="text" id="uri" name="uri" value="/api/song/lyric/v1">
|
||||
<label for="crypto">crypto</label>
|
||||
<select id="crypto" name="crypto">
|
||||
<option value="weapi">weapi</option>
|
||||
<option value="eapi">eapi</option>
|
||||
<option value="api">api</option>
|
||||
<option value="linuxapi">linuxapi</option>
|
||||
<option value="" selected>(默认)</option>
|
||||
</select>
|
||||
<button type="submit">发送</button>
|
||||
</form>
|
||||
<div class="data-result">
|
||||
<div>
|
||||
<label for="result">响应结果</label>
|
||||
<textarea id="result" name="result" readonly></textarea>
|
||||
<label for="result">result</label>
|
||||
<textarea id="result" name="result"></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label for="data">请求数据</label>
|
||||
<label for="data">data</label>
|
||||
<textarea id="data" name="data">
|
||||
{
|
||||
"cp": false,
|
||||
@@ -196,27 +130,27 @@
|
||||
const crypto = params.get('crypto');
|
||||
const dataParam = params.get('data');
|
||||
if (uri) {
|
||||
document.getElementById('uri').value = uri;
|
||||
document.getElementById('uri').value = uri;
|
||||
}
|
||||
if (crypto) {
|
||||
const cryptoSelect = document.getElementById('crypto');
|
||||
if ([...cryptoSelect.options].some((opt) => opt.value === crypto)) {
|
||||
cryptoSelect.value = crypto;
|
||||
}
|
||||
const cryptoSelect = document.getElementById('crypto');
|
||||
if ([...cryptoSelect.options].some((opt) => opt.value === crypto)) {
|
||||
cryptoSelect.value = crypto;
|
||||
}
|
||||
}
|
||||
if (dataParam) {
|
||||
const decoded = dataParam;
|
||||
try {
|
||||
document.getElementById('data').value = JSON.stringify(
|
||||
JSON.parse(decoded),
|
||||
null,
|
||||
2,
|
||||
);
|
||||
} catch (error) {
|
||||
document.getElementById('data').value = decoded;
|
||||
}
|
||||
const decoded = dataParam;
|
||||
try {
|
||||
document.getElementById('data').value = JSON.stringify(
|
||||
JSON.parse(decoded),
|
||||
null,
|
||||
2,
|
||||
);
|
||||
} catch (error) {
|
||||
document.getElementById('data').value = decoded;
|
||||
}
|
||||
}
|
||||
})();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1611,9 +1611,9 @@ function instantiateRuntime(){
|
||||
|
||||
function GenerateFP(floatArray) {
|
||||
let PCMBuffer = Float32Array.from(floatArray)
|
||||
console.info('[afp] input samples n=', PCMBuffer.length)
|
||||
console.log('[afp] input samples n=', PCMBuffer.length)
|
||||
return instantiateRuntime().then((fpRuntime) => {
|
||||
console.info('[afp] begin fingerprinting')
|
||||
console.log('[afp] begin fingerprinting')
|
||||
let fp_vector = fpRuntime.ExtractQueryFP(PCMBuffer.buffer)
|
||||
let result_buf = new Uint8Array(fp_vector.size());
|
||||
for (let t = 0; t < fp_vector.size(); t++)
|
||||
|
||||
@@ -1,142 +1,27 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>听歌识曲 Demo</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 32px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 1px solid #eee;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 14px;
|
||||
color: #555;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 12px;
|
||||
pre {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #333;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.section h3 {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.control-group {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 10px 20px;
|
||||
background: #333;
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
background: #999;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
padding: 10px 14px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.checkbox-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.checkbox-group input[type="checkbox"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.checkbox-group label {
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
color: #555;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
audio {
|
||||
width: 100%;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
canvas {
|
||||
width: 100%;
|
||||
height: 0;
|
||||
transition: all linear 0.1s;
|
||||
background: #f9f9f9;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.canvas-active {
|
||||
@@ -144,80 +29,39 @@
|
||||
}
|
||||
|
||||
pre {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
padding: 16px;
|
||||
background: #f9f9f9;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #eee;
|
||||
}
|
||||
|
||||
.warning {
|
||||
padding: 12px 16px;
|
||||
background: #fef3c7;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
color: #92400e;
|
||||
margin-bottom: 16px;
|
||||
overflow: scroll;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>听歌识曲 Demo</h1>
|
||||
<p class="subtitle">Credit: <a href="https://github.com/mos9527/ncm-afp" target="_blank">https://github.com/mos9527/ncm-afp</a></p>
|
||||
<h1>听歌识曲 Demo (Credit: <a href="https://github.com/mos9527/ncm-afp" target="_blank">https://github.com/mos9527/ncm-afp</a>)</h1>
|
||||
<hr>
|
||||
<p><b>DISCLAIMER: </b></p>
|
||||
<p>This site uses the offical NetEase audio matcher APIs (reverse engineered from <a
|
||||
href="https://fn.music.163.com/g/chrome-extension-home-page-beta/">https://fn.music.163.com/g/chrome-extension-home-page-beta/</a>)
|
||||
</p>
|
||||
<p>And DOES NOT condone copyright infringment nor intellectual property theft.</p>
|
||||
<hr>
|
||||
<p><b>NOTE:</b></p>
|
||||
<p>Before start using the site, you may want to access this link first:</p>
|
||||
<a href="https://cors-anywhere.herokuapp.com/corsdemo">https://cors-anywhere.herokuapp.com/corsdemo</a>
|
||||
<p>Since Netease APIs do not have CORS headers, this is required to alleviate this restriction.</p>
|
||||
<hr>
|
||||
<p>Usage:</p>
|
||||
<li>Select your audio file through "Choose File" picker</li>
|
||||
<li>Hit the "Clip" button and wait for the results!</li>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="warning">
|
||||
<strong>免责声明:</strong>本站点使用网易云音乐官方音频识别API(逆向自 <a href="https://fn.music.163.com/g/chrome-extension-home-page-beta/" target="_blank">Chrome 扩展页面</a>),不鼓励版权侵犯或知识产权盗窃。
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>使用说明</h3>
|
||||
<p>在使用本站点之前,您可能需要先访问以下链接:</p>
|
||||
<p><a href="https://cors-anywhere.herokuapp.com/corsdemo" target="_blank">https://cors-anywhere.herokuapp.com/corsdemo</a></p>
|
||||
<p>由于网易云音乐API没有CORS头,这是解决此限制的必要步骤。</p>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>使用方法</h3>
|
||||
<ul style="padding-left: 20px; font-size: 14px; color: #555;">
|
||||
<li>通过"选择文件"选择您的音频文件</li>
|
||||
<li>点击"识别"按钮并等待结果</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<audio id="audio" controls autoplay></audio>
|
||||
<canvas id="canvas"></canvas>
|
||||
|
||||
<div class="control-group">
|
||||
<button id="invoke">识别</button>
|
||||
<input type="file" name="picker" accept="*" id="file">
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<div class="checkbox-group">
|
||||
<input type="checkbox" name="use-mic" id="usemic">
|
||||
<label for="usemic">混合麦克风输入</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<h3 style="font-size: 16px; font-weight: 600; color: #333; margin-bottom: 12px;">日志</h3>
|
||||
<pre id="logs"></pre>
|
||||
</div>
|
||||
<audio id="audio" controls autoplay></audio>
|
||||
<canvas id="canvas"></canvas>
|
||||
<button id="invoke">Clip</button>
|
||||
<input type="file" name="picker" accept="*" id="file">
|
||||
<hr>
|
||||
<label for="use-mic">Mix in Microphone input</label>
|
||||
<input type="checkbox" name="use-mic" id="usemic">
|
||||
<hr>
|
||||
<pre id="logs"></pre>
|
||||
</body>
|
||||
|
||||
<script src="./afp.wasm.js"></script>
|
||||
<script src="./afp.js"></script>
|
||||
<script type="module">
|
||||
@@ -232,17 +76,13 @@
|
||||
let canvas = document.getElementById('canvas')
|
||||
let canvasCtx = canvas.getContext('2d')
|
||||
let logs = document.getElementById('logs')
|
||||
logs.write = line => {
|
||||
// Append log lines as text to avoid interpreting content as HTML
|
||||
logs.appendChild(document.createTextNode(line));
|
||||
logs.appendChild(document.createElement('br'));
|
||||
}
|
||||
logs.write = line => logs.innerHTML += line + '\n'
|
||||
|
||||
function RecorderCallback(channelL) {
|
||||
let sampleBuffer = new Float32Array(channelL.subarray(0, duration * 8000))
|
||||
GenerateFP(sampleBuffer).then(FP => {
|
||||
logs.write(`[index] 生成指纹 ${FP}`)
|
||||
logs.write('[index] 正在查询,请稍候...')
|
||||
logs.write(`[index] Generated FP ${FP}`)
|
||||
logs.write('[index] Now querying, please wait...')
|
||||
fetch(
|
||||
'/audio/match?' +
|
||||
new URLSearchParams({
|
||||
@@ -251,9 +91,9 @@
|
||||
method: 'POST'
|
||||
}).then(resp => resp.json()).then(resp => {
|
||||
if (!resp.data.result) {
|
||||
return logs.write('[index] 查询失败,无结果')
|
||||
return logs.write('[index] Query failed with no results.')
|
||||
}
|
||||
logs.write(`[index] 查询完成。结果数量=${resp.data.result.length}`)
|
||||
logs.write(`[index] Query complete. Results=${resp.data.result.length}`)
|
||||
for (var song of resp.data.result) {
|
||||
logs.write(
|
||||
`[result] <a target="_blank" href="https://music.163.com/song?id=${song.song.id}">${song.song.name} - ${song.song.album.name} (${song.startTime / 1000}s)</a>`
|
||||
@@ -264,19 +104,20 @@
|
||||
}
|
||||
|
||||
function InitAudioCtx() {
|
||||
// AFP.wasm can't do it with anything other than 8KHz
|
||||
audioCtx = new AudioContext({ 'sampleRate': 8000 })
|
||||
if (audioCtx.state == 'suspended')
|
||||
return false
|
||||
let audioNode = audioCtx.createMediaElementSource(audio)
|
||||
audioCtx.audioWorklet.addModule('rec.js').then(() => {
|
||||
recorderNode = new AudioWorkletNode(audioCtx, 'timed-recorder')
|
||||
audioNode.connect(recorderNode)
|
||||
audioNode.connect(recorderNode) // recorderNode doesn't output anything
|
||||
audioNode.connect(audioCtx.destination)
|
||||
recorderNode.port.onmessage = event => {
|
||||
switch (event.data.message) {
|
||||
case 'finished':
|
||||
RecorderCallback(event.data.recording)
|
||||
clip.innerHTML = '识别'
|
||||
clip.innerHTML = 'Clip'
|
||||
clip.disabled = false
|
||||
canvas.classList.remove('canvas-active')
|
||||
break
|
||||
@@ -289,6 +130,7 @@
|
||||
logs.write(event.data.message)
|
||||
}
|
||||
}
|
||||
// Attempt to get user's microphone and connect it to the AudioContext.
|
||||
navigator.mediaDevices.getUserMedia({
|
||||
audio: {
|
||||
echoCancellation: false,
|
||||
@@ -300,7 +142,7 @@
|
||||
micSourceNode = audioCtx.createMediaStreamSource(micStream);
|
||||
micSourceNode.connect(recorderNode)
|
||||
usemic.checked = true
|
||||
logs.write('[rec.js] 麦克风已连接')
|
||||
logs.write('[rec.js] Microphone attached.')
|
||||
});
|
||||
});
|
||||
return true
|
||||
@@ -319,20 +161,10 @@
|
||||
else
|
||||
micSourceNode.connect(recorderNode)
|
||||
})
|
||||
function escapeHtml(str) {
|
||||
return String(str)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
.replace(/\//g, '/');
|
||||
}
|
||||
file.addEventListener('change', event => {
|
||||
file.files[0].arrayBuffer().then(
|
||||
async buffer => {
|
||||
const safeName = escapeHtml(file.files[0].name)
|
||||
logs.write(`[index] 文件 ${safeName} 已加载`)
|
||||
logs.write(`[index] File ${file.files[0].name} loaded.`)
|
||||
audio.src = window.URL.createObjectURL(new Blob([buffer]))
|
||||
clip.disabled = false
|
||||
})
|
||||
@@ -356,13 +188,12 @@
|
||||
UpdateCanvas()
|
||||
let requestCtx = setInterval(() => {
|
||||
try {
|
||||
if (InitAudioCtx()) {
|
||||
if (InitAudioCtx()) { // Put this here so we don't have to deal with the 'user did not interact' thing
|
||||
clearInterval(requestCtx)
|
||||
logs.write('[rec.js] 音频上下文已启动')
|
||||
logs.write('[rec.js] Audio Context started.')
|
||||
}
|
||||
} catch {
|
||||
// Fail silently
|
||||
}
|
||||
}, 100)
|
||||
</script>
|
||||
</html>
|
||||
@@ -5,320 +5,67 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>更新头像</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 40px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
max-width: 400px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.avatar-wrapper {
|
||||
position: relative;
|
||||
width: 160px;
|
||||
height: 160px;
|
||||
margin: 0 auto 32px;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
border: 3px solid #fff;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.avatar-wrapper.loading .avatar {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.avatar-wrapper.loading .loading-overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 3px solid #e0e0e0;
|
||||
border-top-color: #333;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.upload-btn {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
padding: 12px 28px;
|
||||
background: #333;
|
||||
color: white;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.upload-btn:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
.upload-btn input[type="file"] {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.login-link {
|
||||
display: block;
|
||||
margin-top: 24px;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.login-link:hover {
|
||||
color: #333;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.toast {
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(100px);
|
||||
padding: 12px 20px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: white;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
opacity: 0;
|
||||
transition: all 0.3s ease;
|
||||
z-index: 1000;
|
||||
max-width: 90%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.toast.show {
|
||||
transform: translateX(-50%) translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.toast.success {
|
||||
background: #10b981;
|
||||
}
|
||||
|
||||
.toast.error {
|
||||
background: #ef4444;
|
||||
}
|
||||
|
||||
.toast.info {
|
||||
background: #3b82f6;
|
||||
}
|
||||
|
||||
.hint {
|
||||
margin-top: 20px;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>更新头像</h1>
|
||||
<p class="subtitle">选择一张图片作为您的新头像</p>
|
||||
|
||||
<div class="avatar-wrapper" id="avatarWrapper">
|
||||
<img id="avatar" class="avatar" src="" alt="头像" />
|
||||
<div class="loading-overlay">
|
||||
<div class="spinner"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label class="upload-btn">
|
||||
选择图片
|
||||
<input id="file" type="file" accept="image/*" />
|
||||
</label>
|
||||
|
||||
<a href="/qrlogin-nocookie.html" class="login-link">
|
||||
还没有登录?点击登录
|
||||
<div>
|
||||
<a href="/qrlogin-nocookie.html">
|
||||
如果没登录,请先登录
|
||||
</a>
|
||||
|
||||
<p class="hint">支持 JPG、PNG 格式,建议尺寸 200x200</p>
|
||||
</div>
|
||||
|
||||
<div id="toast" class="toast"></div>
|
||||
|
||||
<script src="https://fastly.jsdelivr.net/npm/[email protected]/dist/axios.min.js"></script>
|
||||
<input id="file" type="file" />
|
||||
<img id="avatar" style="height: 200px; width: 200px; border-radius: 50%" />
|
||||
<script src="https://fastly.jsdelivr.net/npm/[email protected]/dist/axios.min.js
|
||||
"></script>
|
||||
<script>
|
||||
main()
|
||||
|
||||
async function main() {
|
||||
const fileInput = document.querySelector('input[type="file"]');
|
||||
const avatarWrapper = document.getElementById('avatarWrapper');
|
||||
|
||||
fileInput.addEventListener('change', function(e) {
|
||||
const file = this.files[0];
|
||||
if (file) {
|
||||
upload(file);
|
||||
}
|
||||
}, false);
|
||||
|
||||
try {
|
||||
showToast('正在加载头像...', 'info');
|
||||
avatarWrapper.classList.add('loading');
|
||||
const res = await axios({
|
||||
url: `/user/detail?uid=32953014×tamp=${Date.now()}`,
|
||||
withCredentials: true
|
||||
});
|
||||
document.querySelector('#avatar').src = res.data.profile.avatarUrl;
|
||||
hideToast();
|
||||
} catch (error) {
|
||||
hideToast();
|
||||
showToast('加载头像失败,请刷新页面重试', 'error');
|
||||
console.error('加载头像失败:', error);
|
||||
} finally {
|
||||
avatarWrapper.classList.remove('loading');
|
||||
}
|
||||
document.querySelector('input[type="file"]').addEventListener(
|
||||
'change',
|
||||
function (e) {
|
||||
var file = this.files[0]
|
||||
upload(file)
|
||||
},
|
||||
false,
|
||||
)
|
||||
const res = await axios({
|
||||
url: `/user/detail?uid=32953014×tamp=${Date.now()}`,
|
||||
withCredentials: true, //跨域的话必须设置
|
||||
})
|
||||
document.querySelector('#avatar').src = res.data.profile.avatarUrl
|
||||
}
|
||||
|
||||
async function upload(file) {
|
||||
const avatarWrapper = document.getElementById('avatarWrapper');
|
||||
|
||||
if (!file.type.startsWith('image/')) {
|
||||
showToast('请选择图片文件', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
showToast('正在上传头像...', 'info');
|
||||
avatarWrapper.classList.add('loading');
|
||||
|
||||
var formData = new FormData();
|
||||
formData.append('imgFile', file);
|
||||
const imgSize = await getImgSize(file);
|
||||
|
||||
const res = await axios({
|
||||
method: 'post',
|
||||
url: `/avatar/upload?cookie=${localStorage.getItem('cookie')}&imgSize=${imgSize.width}&imgX=0&imgY=0×tamp=${Date.now()}`,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
data: formData,
|
||||
});
|
||||
|
||||
document.querySelector('#avatar').src = res.data.data.url;
|
||||
showToast('头像更新成功!', 'success');
|
||||
} catch (error) {
|
||||
console.error('上传失败:', error);
|
||||
const errorMsg = error.response?.data?.message || error.message || '上传失败,请重试';
|
||||
showToast(errorMsg, 'error');
|
||||
} finally {
|
||||
avatarWrapper.classList.remove('loading');
|
||||
}
|
||||
var formData = new FormData()
|
||||
formData.append('imgFile', file)
|
||||
const imgSize = await getImgSize(file)
|
||||
const res = await axios({
|
||||
method: 'post',
|
||||
url: `/avatar/upload?cookie=${localStorage.getItem('cookie')}&imgSize=${imgSize.width
|
||||
}&imgX=0&imgY=0×tamp=${Date.now()}`,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
data: formData,
|
||||
})
|
||||
document.querySelector('#avatar').src = res.data.data.url
|
||||
}
|
||||
|
||||
function getImgSize(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
reader.onload = function(theFile) {
|
||||
let image = new Image();
|
||||
image.src = theFile.target.result;
|
||||
image.onload = function() {
|
||||
let reader = new FileReader()
|
||||
reader.readAsDataURL(file)
|
||||
reader.onload = function (theFile) {
|
||||
let image = new Image()
|
||||
image.src = theFile.target.result
|
||||
image.onload = function () {
|
||||
resolve({
|
||||
width: this.width,
|
||||
height: this.height,
|
||||
});
|
||||
};
|
||||
image.onerror = function() {
|
||||
reject(new Error('图片加载失败'));
|
||||
};
|
||||
};
|
||||
reader.onerror = function() {
|
||||
reject(new Error('文件读取失败'));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function showToast(message, type = 'info') {
|
||||
const toast = document.getElementById('toast');
|
||||
toast.textContent = message;
|
||||
toast.className = `toast ${type}`;
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
toast.classList.add('show');
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
toast.classList.remove('show');
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
function hideToast() {
|
||||
const toast = document.getElementById('toast');
|
||||
toast.classList.remove('show');
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@@ -1,578 +1,99 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>云盘上传</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
padding: 20px;
|
||||
}
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>云盘上传</title>
|
||||
</head>
|
||||
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 32px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
<body>
|
||||
<div>
|
||||
<a href="/qrlogin-nocookie.html">
|
||||
如果没登录,请先登录
|
||||
</a>
|
||||
</div>
|
||||
<input id="file" type="file" multiple />
|
||||
<div id="app">
|
||||
<ul>
|
||||
<li v-for="(item,index) in songs" :key="index">
|
||||
{{item.songName}}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.login-link {
|
||||
display: block;
|
||||
margin-bottom: 24px;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.login-link:hover {
|
||||
color: #333;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.mode-section {
|
||||
margin-bottom: 24px;
|
||||
padding: 16px;
|
||||
background: #f9f9f9;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.mode-section label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.mode-options {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.mode-option {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mode-option input[type="radio"] {
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.mode-option-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.mode-option-title {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.mode-option-desc {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.mode-option input[type="radio"]:checked + .mode-option-text .mode-option-title {
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.upload-section {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.upload-btn {
|
||||
display: inline-block;
|
||||
padding: 12px 28px;
|
||||
background: #333;
|
||||
color: white;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.upload-btn:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
.upload-btn input[type="file"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.upload-btn.disabled {
|
||||
background: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.songs-list {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.song-item {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #eee;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.song-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.progress-section {
|
||||
margin-bottom: 24px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.progress-section.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.progress-item {
|
||||
margin-bottom: 12px;
|
||||
padding: 12px;
|
||||
background: #f9f9f9;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.progress-item .name {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.progress-item .status {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 6px;
|
||||
background: #e0e0e0;
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-bar .fill {
|
||||
height: 100%;
|
||||
background: #333;
|
||||
border-radius: 3px;
|
||||
transition: width 0.3s ease;
|
||||
width: 0%;
|
||||
}
|
||||
|
||||
.progress-item.success .fill {
|
||||
background: #4caf50;
|
||||
}
|
||||
|
||||
.progress-item.error .fill {
|
||||
background: #f44336;
|
||||
}
|
||||
|
||||
.progress-item.error .status {
|
||||
color: #f44336;
|
||||
}
|
||||
|
||||
.info-text {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.warning-text {
|
||||
font-size: 12px;
|
||||
color: #e65100;
|
||||
margin-top: 8px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>云盘上传</h1>
|
||||
<a href="/qrlogin-nocookie.html" class="login-link">还没登录?点击登录</a>
|
||||
|
||||
<div class="mode-section">
|
||||
<label>上传模式</label>
|
||||
<div class="mode-options">
|
||||
<label class="mode-option">
|
||||
<input type="radio" name="uploadMode" value="direct" checked />
|
||||
<span class="mode-option-text">
|
||||
<span class="mode-option-title">客户端直传</span>
|
||||
<span class="mode-option-desc">文件直接上传到云存储,支持大文件,适合 Vercel 等平台</span>
|
||||
</span>
|
||||
</label>
|
||||
<label class="mode-option">
|
||||
<input type="radio" name="uploadMode" value="proxy" />
|
||||
<span class="mode-option-text">
|
||||
<span class="mode-option-title">后端代理</span>
|
||||
<span class="mode-option-desc">文件通过服务器转发,更简洁,需要服务器支持大文件</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="upload-section">
|
||||
<label class="upload-btn" id="uploadBtn">
|
||||
选择文件(支持多选)
|
||||
<input id="file" type="file" multiple accept="audio/*" />
|
||||
</label>
|
||||
<p class="info-text" id="modeInfo">支持大文件上传,文件将直接传输到云存储服务器</p>
|
||||
</div>
|
||||
|
||||
<div id="progressSection" class="progress-section"></div>
|
||||
|
||||
<div id="app">
|
||||
<div v-if="loading" class="loading">加载中...</div>
|
||||
<ul v-else-if="songs.length > 0" class="songs-list">
|
||||
<li v-for="(item, index) in songs" :key="index" class="song-item">
|
||||
{{ item.songName }}
|
||||
</li>
|
||||
</ul>
|
||||
<div v-else class="empty-state">暂无云盘歌曲</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://fastly.jsdelivr.net/npm/[email protected]/dist/axios.min.js"></script>
|
||||
<script src="https://fastly.jsdelivr.net/npm/vue@3"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jsmediatags/3.9.5/jsmediatags.min.js"></script>
|
||||
<script>
|
||||
const app = Vue.createApp({
|
||||
data() {
|
||||
return {
|
||||
songs: [],
|
||||
loading: false,
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.getData()
|
||||
},
|
||||
methods: {
|
||||
getData() {
|
||||
this.loading = true
|
||||
axios({
|
||||
url: `/user/cloud?time=${Date.now()}&cookie=${localStorage.getItem('cookie')}`,
|
||||
})
|
||||
.then((res) => {
|
||||
this.songs = res.data.data || []
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('获取云盘数据失败:', err)
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
},
|
||||
}).mount('#app')
|
||||
|
||||
let isUploading = false
|
||||
let uploadMode = 'direct'
|
||||
const progressSection = document.getElementById('progressSection')
|
||||
const uploadBtn = document.getElementById('uploadBtn')
|
||||
const fileInput = document.querySelector('input[type="file"]')
|
||||
const modeInfo = document.getElementById('modeInfo')
|
||||
|
||||
document.querySelectorAll('input[name="uploadMode"]').forEach(radio => {
|
||||
radio.addEventListener('change', function() {
|
||||
uploadMode = this.value
|
||||
if (uploadMode === 'direct') {
|
||||
modeInfo.textContent = '支持大文件上传,文件将直接传输到云存储服务器'
|
||||
modeInfo.className = 'info-text'
|
||||
} else {
|
||||
modeInfo.textContent = '文件将通过服务器转发,服务器需支持大文件上传(Vercel 限制 4.5MB)'
|
||||
modeInfo.className = 'warning-text'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
function main() {
|
||||
fileInput.addEventListener('change', function (e) {
|
||||
const files = this.files
|
||||
if (files.length === 0) return
|
||||
if (isUploading) return
|
||||
|
||||
uploadFilesSequentially(Array.from(files))
|
||||
this.value = ''
|
||||
})
|
||||
}
|
||||
main()
|
||||
|
||||
async function uploadFilesSequentially(files) {
|
||||
isUploading = true
|
||||
uploadBtn.classList.add('disabled')
|
||||
progressSection.classList.add('active')
|
||||
progressSection.innerHTML = ''
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
if (uploadMode === 'direct') {
|
||||
await uploadFileDirect(files[i], i + 1, files.length)
|
||||
} else {
|
||||
await uploadFileProxy(files[i], i + 1, files.length)
|
||||
}
|
||||
<script src="https://fastly.jsdelivr.net/npm/[email protected]/dist/axios.min.js"></script>
|
||||
<script src="https://fastly.jsdelivr.net/npm/vue"></script>
|
||||
<script>
|
||||
const app = Vue.createApp({
|
||||
data() {
|
||||
return {
|
||||
songs: []
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.getData()
|
||||
},
|
||||
methods: {
|
||||
getData() {
|
||||
console.log('getdata');
|
||||
const _this = this
|
||||
axios({
|
||||
url: `/user/cloud?time=${Date.now()}&cookie=${localStorage.getItem('cookie')}`,
|
||||
}).then(res => {
|
||||
console.log(res.data)
|
||||
_this.songs = res.data.data
|
||||
})
|
||||
}
|
||||
}
|
||||
}).mount('#app')
|
||||
const fileUpdateTime = {}
|
||||
let fileLength = 0
|
||||
|
||||
isUploading = false
|
||||
uploadBtn.classList.remove('disabled')
|
||||
function main() {
|
||||
document
|
||||
.querySelector('input[type="file"]')
|
||||
.addEventListener('change', function (e) {
|
||||
console.log(this.files)
|
||||
let currentIndx = 0
|
||||
fileLength = this.files.length
|
||||
for (const item of this.files) {
|
||||
currentIndx += 1
|
||||
upload(item, currentIndx)
|
||||
}
|
||||
})
|
||||
}
|
||||
main()
|
||||
|
||||
function upload(file, currentIndx) {
|
||||
var formData = new FormData()
|
||||
formData.append('songFile', file)
|
||||
axios({
|
||||
method: 'post',
|
||||
url: `/cloud?time=${Date.now()}&cookie=${localStorage.getItem('cookie')}`,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
data: formData,
|
||||
}).then(res => {
|
||||
console.log(`${file.name} 上传成功`)
|
||||
if (currentIndx >= fileLength) { console.log('上传完毕') }
|
||||
app.getData()
|
||||
}
|
||||
|
||||
function createProgressItem(file, index, total) {
|
||||
const item = document.createElement('div')
|
||||
item.className = 'progress-item'
|
||||
item.id = `progress-${index}`
|
||||
item.innerHTML = `
|
||||
<div class="name">${file.name} (${formatSize(file.size)})</div>
|
||||
<div class="status">准备中...</div>
|
||||
<div class="progress-bar"><div class="fill"></div></div>
|
||||
`
|
||||
progressSection.appendChild(item)
|
||||
return item
|
||||
}
|
||||
|
||||
function updateProgress(index, status, percent, isError = false) {
|
||||
const item = document.getElementById(`progress-${index}`)
|
||||
if (!item) return
|
||||
item.querySelector('.status').textContent = status
|
||||
item.querySelector('.fill').style.width = `${percent}%`
|
||||
if (isError) {
|
||||
item.classList.add('error')
|
||||
} else if (percent >= 100) {
|
||||
item.classList.add('success')
|
||||
}).catch(async err => {
|
||||
console.log(err)
|
||||
console.log(fileUpdateTime)
|
||||
fileUpdateTime[file.name] ? fileUpdateTime[file.name] += 1 : fileUpdateTime[file.name] = 1
|
||||
if (fileUpdateTime[file.name] >= 4) {
|
||||
console.error(`丢,这首歌怎么都传不上:${file.name}`)
|
||||
return
|
||||
} else {
|
||||
console.error(`${file.name} 失败 ${fileUpdateTime[file.name]} 次`)
|
||||
}
|
||||
}
|
||||
// await login()
|
||||
upload(file, currentIndx)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
function formatSize(bytes) {
|
||||
if (bytes < 1024) return bytes + ' B'
|
||||
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'
|
||||
return (bytes / (1024 * 1024)).toFixed(1) + ' MB'
|
||||
}
|
||||
|
||||
async function uploadFileProxy(file, index, total) {
|
||||
createProgressItem(file, index, total)
|
||||
|
||||
try {
|
||||
updateProgress(index, '上传中...', 10)
|
||||
|
||||
const formData = new FormData()
|
||||
formData.append('songFile', file)
|
||||
|
||||
await axios({
|
||||
method: 'post',
|
||||
url: `/cloud?time=${Date.now()}&cookie=${localStorage.getItem('cookie')}`,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
data: formData,
|
||||
onUploadProgress: (progressEvent) => {
|
||||
const percent = Math.round((progressEvent.loaded / progressEvent.total) * 90) + 10
|
||||
updateProgress(index, `上传中... ${Math.round(progressEvent.loaded / progressEvent.total * 100)}%`, Math.min(percent, 100))
|
||||
},
|
||||
timeout: 600000,
|
||||
})
|
||||
|
||||
updateProgress(index, '上传完成!', 100)
|
||||
|
||||
} catch (err) {
|
||||
console.error(`${file.name} 上传失败:`, err)
|
||||
const errorMsg = err.response?.data?.msg || err.message || '未知错误'
|
||||
if (err.response?.status === 413 || errorMsg.includes('PAYLOAD_TOO_LARGE')) {
|
||||
updateProgress(index, '文件过大,请切换到客户端直传模式', 0, true)
|
||||
} else {
|
||||
updateProgress(index, `上传失败: ${errorMsg}`, 0, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function calculateMD5(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const chunkSize = 2 * 1024 * 1024
|
||||
const chunks = Math.ceil(file.size / chunkSize)
|
||||
let currentChunk = 0
|
||||
const spark = new SparkMD5.ArrayBuffer()
|
||||
const reader = new FileReader()
|
||||
|
||||
reader.onload = (e) => {
|
||||
spark.append(e.target.result)
|
||||
currentChunk++
|
||||
if (currentChunk < chunks) {
|
||||
loadNext()
|
||||
} else {
|
||||
resolve(spark.end())
|
||||
}
|
||||
}
|
||||
|
||||
reader.onerror = () => reject(reader.error)
|
||||
|
||||
function loadNext() {
|
||||
const start = currentChunk * chunkSize
|
||||
const end = Math.min(start + chunkSize, file.size)
|
||||
reader.readAsArrayBuffer(file.slice(start, end))
|
||||
}
|
||||
|
||||
loadNext()
|
||||
})
|
||||
}
|
||||
|
||||
async function parseMediaTags(file) {
|
||||
return new Promise((resolve) => {
|
||||
jsmediatags.read(file, {
|
||||
onSuccess: function(tag) {
|
||||
resolve({
|
||||
title: tag.tags.title || null,
|
||||
artist: tag.tags.artist || null,
|
||||
album: tag.tags.album || null,
|
||||
})
|
||||
},
|
||||
onError: function() {
|
||||
resolve({ title: null, artist: null, album: null })
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async function uploadFileDirect(file, index, total) {
|
||||
createProgressItem(file, index, total)
|
||||
|
||||
try {
|
||||
updateProgress(index, '计算文件MD5...', 5)
|
||||
|
||||
const md5 = await calculateMD5(file)
|
||||
const fileSize = file.size
|
||||
const filename = file.name
|
||||
|
||||
updateProgress(index, '解析音频元数据...', 8)
|
||||
|
||||
const mediaTags = await parseMediaTags(file)
|
||||
|
||||
updateProgress(index, '获取上传凭证...', 10)
|
||||
|
||||
const tokenRes = await axios({
|
||||
method: 'post',
|
||||
url: `/cloud/upload/token?time=${Date.now()}`,
|
||||
data: {
|
||||
cookie: localStorage.getItem('cookie'),
|
||||
md5: md5,
|
||||
fileSize: fileSize,
|
||||
filename: filename,
|
||||
},
|
||||
})
|
||||
|
||||
if (tokenRes.data.code !== 200) {
|
||||
throw new Error(tokenRes.data.msg || '获取上传凭证失败')
|
||||
}
|
||||
|
||||
const tokenData = tokenRes.data.data
|
||||
|
||||
if (!tokenData.needUpload) {
|
||||
updateProgress(index, '文件已存在,直接导入云盘...', 80)
|
||||
await completeUpload(tokenData, file, mediaTags)
|
||||
updateProgress(index, '上传完成!', 100)
|
||||
return
|
||||
}
|
||||
|
||||
updateProgress(index, '开始上传到云存储...', 15)
|
||||
|
||||
await axios({
|
||||
method: 'post',
|
||||
url: tokenData.uploadUrl,
|
||||
headers: {
|
||||
'x-nos-token': tokenData.uploadToken,
|
||||
'Content-MD5': md5,
|
||||
'Content-Type': 'audio/mpeg',
|
||||
'Content-Length': String(fileSize),
|
||||
},
|
||||
data: file,
|
||||
onUploadProgress: (progressEvent) => {
|
||||
const percent = Math.round((progressEvent.loaded / progressEvent.total) * 70) + 15
|
||||
updateProgress(index, `上传中... ${Math.round(progressEvent.loaded / progressEvent.total * 100)}%`, Math.min(percent, 85))
|
||||
},
|
||||
maxContentLength: Infinity,
|
||||
maxBodyLength: Infinity,
|
||||
timeout: 600000,
|
||||
})
|
||||
|
||||
updateProgress(index, '上传完成,正在导入云盘...', 90)
|
||||
|
||||
await completeUpload(tokenData, file, mediaTags)
|
||||
|
||||
updateProgress(index, '上传完成!', 100)
|
||||
|
||||
} catch (err) {
|
||||
console.error(`${file.name} 上传失败:`, err)
|
||||
const errorMsg = err.response?.data?.msg || err.message || '未知错误'
|
||||
updateProgress(index, `上传失败: ${errorMsg}`, 0, true)
|
||||
}
|
||||
}
|
||||
|
||||
async function completeUpload(tokenData, file, mediaTags = {}) {
|
||||
const songName = mediaTags.title || file.name.replace(/\.[^.]+$/, '')
|
||||
const artist = mediaTags.artist || '未知艺术家'
|
||||
const album = mediaTags.album || '未知专辑'
|
||||
|
||||
const completeRes = await axios({
|
||||
method: 'post',
|
||||
url: `/cloud/upload/complete?time=${Date.now()}`,
|
||||
data: {
|
||||
cookie: localStorage.getItem('cookie'),
|
||||
songId: tokenData.songId,
|
||||
resourceId: tokenData.resourceId,
|
||||
md5: tokenData.md5,
|
||||
filename: file.name,
|
||||
song: songName,
|
||||
artist: artist,
|
||||
album: album,
|
||||
},
|
||||
})
|
||||
|
||||
if (completeRes.data.code !== 200) {
|
||||
throw new Error(completeRes.data.msg || '导入云盘失败')
|
||||
}
|
||||
|
||||
return completeRes.data
|
||||
}
|
||||
</script>
|
||||
<script src="https://fastly.jsdelivr.net/npm/[email protected]/spark-md5.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
# 网易云音乐 API Enhanced
|
||||
# 网易云音乐 API
|
||||
|
||||
> 🔍 网易云音乐API Node.js服务的复兴项目
|
||||
> 网易云音乐 NodeJS 版 API
|
||||
|
||||
- 基于原版网易云API新增更多有趣的功能
|
||||
- 全部接口已升级到最新
|
||||
- 具备登录接口,多达200多个接口
|
||||
- 更完善的文档
|
||||
|
||||
|
||||
[Github](https://github.com/neteasecloudmusicapienhanced/api-enhanced)
|
||||
[Get Started](#neteasecloudmusicapienhanced)
|
||||
[GitLab](https://gitlab.com/Binaryify/NeteaseCloudMusicApi)
|
||||
[Get Started](#neteasecloudmusicapi)
|
||||
|
||||

|
||||
|
||||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 858 KiB After Width: | Height: | Size: 15 KiB |
@@ -3,56 +3,13 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="KEYWords" contect="网易云音乐,网易云音乐 api,网易云音乐 nodejs,网易云音乐 node.js">
|
||||
<meta name="description" content="网易云音乐 NodeJS API Enhanced">
|
||||
<title>网易云音乐 NodeJS API Enhanced</title>
|
||||
<link rel="icon" href="netease.png">
|
||||
<meta name="description" content="网易云音乐 NodeJS 版 API">
|
||||
<title>网易云音乐 NodeJS 版 API</title>
|
||||
<link rel="icon" href="favicon.ico">
|
||||
<meta name="description" content="Description">
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
||||
<meta name="referrer" content="never">
|
||||
<link rel="stylesheet" href="//unpkg.com/docsify/lib/themes/vue.css" id="theme-css">
|
||||
<style>
|
||||
.theme-toggle {
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
right: 24px;
|
||||
z-index: 999;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid rgba(255,255,255,0.1);
|
||||
background: rgba(30,30,46,0.75);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
color: #fff;
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 2px 12px rgba(0,0,0,0.15);
|
||||
transition: all 0.3s cubic-bezier(0.4,0,0.2,1);
|
||||
outline: none;
|
||||
padding: 0;
|
||||
line-height: 1;
|
||||
user-select: none;
|
||||
}
|
||||
.theme-toggle:hover {
|
||||
transform: scale(1.12) rotate(12deg);
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.25);
|
||||
}
|
||||
.theme-toggle:active {
|
||||
transform: scale(0.92);
|
||||
}
|
||||
.theme-toggle.light {
|
||||
background: rgba(255,255,255,0.85);
|
||||
color: #333;
|
||||
border-color: rgba(0,0,0,0.08);
|
||||
}
|
||||
html, body, .sidebar, .content, .markdown-section,
|
||||
.sidebar-nav, .search, .docsify-pagination-container {
|
||||
transition: background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease;
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" href="//unpkg.com/docsify/lib/themes/vue.css">
|
||||
<script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
|
||||
<script>
|
||||
(adsbygoogle = window.adsbygoogle || []).push({
|
||||
@@ -63,52 +20,13 @@
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<button class="theme-toggle" id="themeToggle" aria-label="Toggle theme">🌙</button>
|
||||
</body>
|
||||
<script>
|
||||
(function() {
|
||||
var STORAGE_KEY = 'docsify-theme';
|
||||
var link = document.getElementById('theme-css');
|
||||
var btn = document.getElementById('themeToggle');
|
||||
var LIGHT_URL = '//unpkg.com/docsify/lib/themes/vue.css';
|
||||
var DARK_URL = '//unpkg.com/docsify/lib/themes/dark.css';
|
||||
|
||||
function setTheme(theme) {
|
||||
link.href = theme === 'dark' ? DARK_URL : LIGHT_URL;
|
||||
btn.textContent = theme === 'dark' ? '☀️' : '🌙';
|
||||
btn.className = 'theme-toggle' + (theme === 'dark' ? '' : ' light');
|
||||
localStorage.setItem(STORAGE_KEY, theme);
|
||||
}
|
||||
|
||||
function toggleTheme() {
|
||||
var isDark = link.href.includes('dark');
|
||||
setTheme(isDark ? 'light' : 'dark');
|
||||
}
|
||||
|
||||
var saved = localStorage.getItem(STORAGE_KEY);
|
||||
if (saved) {
|
||||
setTheme(saved);
|
||||
} else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||
setTheme('dark');
|
||||
}
|
||||
|
||||
btn.addEventListener('click', toggleTheme);
|
||||
|
||||
try {
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function(e) {
|
||||
if (!localStorage.getItem(STORAGE_KEY)) {
|
||||
setTheme(e.matches ? 'dark' : 'light');
|
||||
}
|
||||
});
|
||||
} catch(e) {}
|
||||
})();
|
||||
</script>
|
||||
<script>
|
||||
window.$docsify = {
|
||||
name: '网易云音乐 API Enhanced',
|
||||
repo: 'https://github.com/neteasecloudmusicapienhanced/api-enhanced',
|
||||
name: '网易云音乐 API',
|
||||
repo: 'https://gitlab.com/Binaryify/NeteaseCloudMusicApi',
|
||||
coverpage: true,
|
||||
homepage: 'https://cdn.jsdelivr.net/gh/NeteaseCloudMusicApiEnhanced/api-enhanced@main/public/docs/home.md',
|
||||
homepage: 'home.md',
|
||||
}
|
||||
</script>
|
||||
<script src="https://unpkg.com/[email protected]/lib/docsify.min.js"></script>
|
||||
@@ -118,12 +36,12 @@
|
||||
}
|
||||
</script>
|
||||
<!-- Global site tag (gtag.js) - Google Analytics -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-BPRGR23JEG"></script>
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-139996012-1"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag() { dataLayer.push(arguments); }
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
|
||||
gtag('config', 'G-BPRGR23JEG');
|
||||
gtag('config', 'UA-139996012-1');
|
||||
</script>
|
||||
</html>
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<g>
|
||||
<path fill="none" d="M0 0h24v24H0z"/>
|
||||
<path d="M10.421 11.375c-.294 1.028.012 2.064.784 2.653 1.061.81 2.565.3 2.874-.995.08-.337.103-.722.027-1.056-.23-1.001-.52-1.988-.792-2.996-1.33.154-2.543 1.172-2.893 2.394zm5.548-.287c.273 1.012.285 2.017-.127 3-1.128 2.69-4.721 3.14-6.573.826-1.302-1.627-1.28-3.961.06-5.734.78-1.032 1.804-1.707 3.048-2.054l.379-.104c-.084-.415-.188-.816-.243-1.224-.176-1.317.512-2.503 1.744-3.04 1.226-.535 2.708-.216 3.53.76.406.479.395 1.08-.025 1.464-.412.377-.996.346-1.435-.09-.247-.246-.51-.44-.877-.436-.525.006-.987.418-.945.937.037.468.173.93.3 1.386.022.078.216.135.338.153 1.334.197 2.504.731 3.472 1.676 2.558 2.493 2.861 6.531.672 9.44-1.529 2.032-3.61 3.168-6.127 3.409-4.621.44-8.664-2.53-9.7-7.058C2.515 10.255 4.84 5.831 8.795 4.25c.586-.234 1.143-.031 1.371.498.232.537-.019 1.086-.61 1.35-2.368 1.06-3.817 2.855-4.215 5.424-.533 3.433 1.656 6.776 5 7.72 2.723.77 5.658-.166 7.308-2.33 1.586-2.08 1.4-5.099-.427-6.873a3.979 3.979 0 0 0-1.823-1.013c.198.716.389 1.388.57 2.062z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.1 MiB |
@@ -1,186 +1,62 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>eapi 参数和返回内容解析</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 32px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #555;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
resize: vertical;
|
||||
min-height: 200px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
textarea:focus {
|
||||
border-color: #333;
|
||||
}
|
||||
|
||||
.radio-group {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.radio-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.radio-item input[type="radio"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.radio-item label {
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
button {
|
||||
background: #333;
|
||||
color: white;
|
||||
padding: 12px 28px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
button + button {
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
.result-section {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.result-section label {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.decode-result {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
background: #f9f9f9;
|
||||
padding: 16px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #eee;
|
||||
min-height: 200px;
|
||||
max-height: 400px;
|
||||
overflow: auto;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.example-section {
|
||||
margin-top: 32px;
|
||||
padding-top: 24px;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
.example-section h2 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.example-section img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 16px;
|
||||
border: 1px solid #eee;
|
||||
}
|
||||
</style>
|
||||
<link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet">
|
||||
</head>
|
||||
|
||||
<style>
|
||||
.decode-result {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
background-color: #f0f0f0;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
margin-top: 10px;
|
||||
height: 300px;
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
<body>
|
||||
<div id="app" class="container">
|
||||
<h1>eapi 参数和返回内容解析</h1>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="hexString">十六进制字符串</label>
|
||||
<textarea id="hexString" v-model="hexString" rows="10"></textarea>
|
||||
<div id="app" class="p-5 flex flex-col">
|
||||
<h1 class="text-2xl font-bold mb-5">eapi 参数和返回内容解析</h1>
|
||||
<textarea class="border border-gray-300 p-3 mb-5" v-model="hexString" rows="10"></textarea>
|
||||
<div class="flex gap-3 mb-4">
|
||||
<button @click="decrypt" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
|
||||
解密
|
||||
</button>
|
||||
<button
|
||||
@click="sendToApi"
|
||||
:disabled="!canSend"
|
||||
:class="[
|
||||
'bg-green-500 hover:bg-green-600 text-white font-bold py-2 px-4 rounded',
|
||||
{ 'opacity-50 cursor-not-allowed pointer-events-none': !canSend },
|
||||
]"
|
||||
>
|
||||
填入 API 调试
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<input type="radio" id="format" name="format" v-model="isReq" value="true">
|
||||
<label for="format" class="ml-2">请求数据request params(针对请求数据的 params)</label>
|
||||
<input type="radio" id="noFormat" name="format" v-model="isReq" value="false" class="ml-5">
|
||||
<label for="noFormat" class="ml-2">返回数据 response 二进制数据(针对返回内容解析)</label>
|
||||
</div>
|
||||
<div>
|
||||
<p>解密结果:
|
||||
<pre class="decode-result">{{ JSON.stringify(JSON.parse(result), null, 2) }}</pre>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="radio-group">
|
||||
<div class="radio-item">
|
||||
<input type="radio" id="req" name="format" v-model="isReq" value="true">
|
||||
<label for="req">请求数据 request params</label>
|
||||
</div>
|
||||
<div class="radio-item">
|
||||
<input type="radio" id="resp" name="format" v-model="isReq" value="false">
|
||||
<label for="resp">返回数据 response 二进制数据</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button @click="decrypt">解密</button>
|
||||
<button @click="sendToApi" :disabled="!canSend" :class="[{ 'opacity-50 cursor-not-allowed pointer-events-none': !canSend },]">填入 API 调试</button>
|
||||
|
||||
<div class="result-section">
|
||||
<label>解密结果:</label>
|
||||
<pre class="decode-result">{{ formatResult(result) }}</pre>
|
||||
</div>
|
||||
|
||||
<div class="example-section">
|
||||
<h2>使用示例</h2>
|
||||
<img src="/static/eapi_params.png" alt="请求示例" />
|
||||
<img src="/static/eapi_response.png" alt="响应示例" />
|
||||
<div>
|
||||
<p>使用例子:</p>
|
||||
<img src="/static/eapi_params.png" />
|
||||
<img src="/static/eapi_response.png" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -215,15 +91,6 @@
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
formatResult(value) {
|
||||
if (value == null || value === '') return ''
|
||||
try {
|
||||
const parsed = typeof value === 'string' ? JSON.parse(value) : value
|
||||
return JSON.stringify(parsed, null, 2)
|
||||
} catch (error) {
|
||||
return String(value)
|
||||
}
|
||||
},
|
||||
async decrypt() {
|
||||
try {
|
||||
const res = await axios({
|
||||
@@ -262,4 +129,4 @@
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -1,40 +1,44 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>home</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div>
|
||||
<a href="/qrlogin-nocookie.html"> 如果没登录,请先登录 </a>
|
||||
</div>
|
||||
<script src="https://fastly.jsdelivr.net/npm/[email protected]/dist/axios.min.js"></script>
|
||||
<script>
|
||||
async function main() {
|
||||
const res = await axios({
|
||||
url: `/homepage/block/page`,
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>home</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div>
|
||||
<a href="/qrlogin-nocookie.html">
|
||||
如果没登录,请先登录
|
||||
</a>
|
||||
</div>
|
||||
<script src="https://fastly.jsdelivr.net/npm/[email protected]/dist/axios.min.js"></script>
|
||||
<script>
|
||||
async function main() {
|
||||
const res = await axios({
|
||||
url: `/homepage/block/page`,
|
||||
data: {
|
||||
cookie: localStorage.getItem('cookie'),
|
||||
},
|
||||
method: 'post',
|
||||
})
|
||||
let cursor = ''
|
||||
console.log(res.data.data)
|
||||
if (res.data.data.hasMore) {
|
||||
cursor = res.data.data.cursor
|
||||
const res2 = await axios({
|
||||
url: `/homepage/block/page?time=${Date.now()}`,
|
||||
data: {
|
||||
cookie: localStorage.getItem('cookie'),
|
||||
cursor: cursor,
|
||||
},
|
||||
method: 'post',
|
||||
})
|
||||
let cursor = ''
|
||||
console.info(res.data.data)
|
||||
if (res.data.data.hasMore) {
|
||||
cursor = res.data.data.cursor
|
||||
const res2 = await axios({
|
||||
url: `/homepage/block/page?time=${Date.now()}`,
|
||||
data: {
|
||||
cookie: localStorage.getItem('cookie'),
|
||||
cursor: cursor,
|
||||
},
|
||||
method: 'post',
|
||||
})
|
||||
}
|
||||
}
|
||||
main()
|
||||
</script>
|
||||
</body>
|
||||
}
|
||||
main()
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
@@ -1,125 +1,138 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<html lang="zh">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<link rel="icon" href="docs/netease.png">
|
||||
<title>网易云音乐 API Enhanced</title>
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>网易云音乐 API</title>
|
||||
<style>
|
||||
:root {
|
||||
--fg: #333;
|
||||
--muted: #666;
|
||||
--border: #ddd;
|
||||
--bg: #f5f5f5;
|
||||
--panel: #ffffff;
|
||||
--accent: #333;
|
||||
html, body {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: #f9f9f9;
|
||||
font-family: Arial, sans-serif;
|
||||
color: #333;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
html, body { height: 100%; }
|
||||
body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; color: var(--fg); background: var(--bg); line-height: 1.6; }
|
||||
.container { max-width: 960px; margin: 40px auto; padding: 0 20px; }
|
||||
@media (max-width: 480px) {
|
||||
.container { margin: 20px auto; padding: 0 16px; }
|
||||
header.site-header h1 { font-size: 22px; }
|
||||
.block { padding: 16px; }
|
||||
|
||||
h1 {
|
||||
font-size: 2.5em;
|
||||
margin-bottom: 1em;
|
||||
color: #42b983;
|
||||
}
|
||||
header.site-header { margin-bottom: 24px; }
|
||||
header.site-header h1 { font-size: 28px; font-weight: 600; margin: 0; }
|
||||
.badge { display: inline-block; margin-left: 8px; padding: 4px 10px; border: 1px solid var(--border); border-radius: 12px; font-size: 12px; color: var(--muted); }
|
||||
.sub { margin-top: 8px; color: var(--muted); font-size: 14px; }
|
||||
.block { background: var(--panel); border: 1px solid var(--border); border-radius: 12px; padding: 20px; margin-bottom: 16px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); }
|
||||
.block h2 { margin: 0 0 12px; font-size: 18px; font-weight: 600; }
|
||||
.kvs { display: grid; grid-template-columns: 100px 1fr; gap: 8px 12px; align-items: start; }
|
||||
.kvs div:first-child { color: var(--muted); flex-shrink: 0; }
|
||||
.kvs div:last-child { word-break: break-all; overflow-wrap: anywhere; min-width: 0; overflow: hidden; }
|
||||
@media (max-width: 480px) {
|
||||
.kvs { grid-template-columns: 1fr; gap: 4px 12px; }
|
||||
.kvs div:first-child { font-weight: 500; }
|
||||
|
||||
p {
|
||||
font-size: 1.2em;
|
||||
margin-bottom: 2em;
|
||||
color: #666;
|
||||
}
|
||||
ul.links { list-style: none; padding: 0; margin: 0; }
|
||||
ul.links li { margin: 8px 0; }
|
||||
ul.links a { color: var(--fg); text-decoration: none; border-bottom: 1px dotted var(--border); transition: all 0.2s ease; }
|
||||
ul.links a:hover { color: var(--accent); border-bottom-color: var(--accent); }
|
||||
pre { margin: 0; background: #f9f9f9; border: 1px solid var(--border); border-radius: 6px; padding: 12px; overflow-x: auto; white-space: pre-wrap; word-break: break-all; }
|
||||
code { font-family: 'Courier New', monospace; font-size: 13px; }
|
||||
@media (max-width: 480px) {
|
||||
code { font-size: 12px; }
|
||||
|
||||
a {
|
||||
color: #42b983;
|
||||
text-decoration: none;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #329d6e;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
text-align: left;
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
li {
|
||||
margin: 0.5em 0;
|
||||
line-height: 1.5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.number {
|
||||
margin-right: 10px;
|
||||
color: #999;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.container {
|
||||
background: #fff;
|
||||
padding: 2em;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
max-height: 80vh; /* 设置最大高度 */
|
||||
overflow-y: auto; /* 启用垂直滚动条 */
|
||||
}
|
||||
|
||||
.version {
|
||||
margin-top: 0.5em;
|
||||
font-size: 1.0em;
|
||||
color: #666;
|
||||
}
|
||||
footer.site-footer { margin-top: 24px; padding-top: 12px; border-top: 1px solid var(--border); color: var(--muted); text-align: center; }
|
||||
footer.site-footer a { color: var(--fg); text-decoration: none; transition: color 0.2s ease; }
|
||||
footer.site-footer a:hover { color: var(--accent); }
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<main class="container">
|
||||
<header class="site-header">
|
||||
<h1>网易云音乐 API Enhanced <span id="api-version" class="badge"></span></h1>
|
||||
<p class="sub">🔍 A revival project for NeteaseCloudMusicApi Node.js Api Services || 网易云音乐 API 备份 + 增强 || 本项目自原版v4.28.0版本后开始自行维护</p>
|
||||
</header>
|
||||
|
||||
<section class="block">
|
||||
<h2>状态</h2>
|
||||
<div class="kvs">
|
||||
<div>Base URL</div><div id="base-url">—</div>
|
||||
<div>当前页</div><div id="current-url">—</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="block">
|
||||
<h2>文档</h2>
|
||||
<p><a href="/docs" target="_blank">查看在线文档</a></p>
|
||||
</section>
|
||||
|
||||
<section class="block">
|
||||
<h2>常用接口</h2>
|
||||
<ul class="links">
|
||||
<li><a href="/search?keywords=妖精小姐的魔法邀约">搜索音乐: <code>GET /search</code></a></li>
|
||||
<li><a href="/song/detail?ids=2756058128">获取音乐详情: <code>GET /song/detail</code></a></li>
|
||||
<li><a href="/comment/music?id=2756058128&limit=1">获取音乐评论: <code>GET /comment/music</code></a></li>
|
||||
<li><a href="/song/url/v1?id=2756058128&level=exhigh">获取音乐播放链接: <code>GET /song/url/v1</code></a></li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="block">
|
||||
<h2>调试部分</h2>
|
||||
<pre><code>curl -s {origin}/inner/version
|
||||
curl -s {origin}/search?keywords=网易云</code></pre>
|
||||
<div style="margin-top:10px; line-height:2;">
|
||||
<a href="/api.html">交互式调试</a> ·
|
||||
<a href="/qrlogin.html">二维码登录示例</a> ·
|
||||
<a href="/unblock_test.html">解灰测试</a> ·
|
||||
<a href="/audio_match_demo/index.html">听歌识曲 Demo</a> ·
|
||||
<a href="/cloud.html">云盘上传</a> ·
|
||||
<a href="/playlist_import.html">歌单导入</a> ·
|
||||
<a href="/eapi_decrypt.html">EAPI 解密</a> ·
|
||||
<a href="/listen_together_host.html">一起听示例</a> ·
|
||||
<a href="/playlist_cover_update.html">更新歌单封面示例</a> ·
|
||||
<a href="/avatar_update.html">头像更新示例</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer class="site-footer">
|
||||
<a href="https://github.com/neteasecloudmusicapienhanced/api-enhanced" target="_blank">GitHub</a>
|
||||
</footer>
|
||||
</main>
|
||||
<div class="container">
|
||||
<h1>网易云音乐 API 二改<span id="api-version"></span></h1>
|
||||
<p>当你看到这个页面时,这个服务已经成功跑起来了~</p>
|
||||
<p>本项目基于原版API二改, 添加了解灰接口</p>
|
||||
<p>本项目更新是及时的</p>
|
||||
<p class="current-url"><span id="current-url"></span></p>
|
||||
<a href="/docs">查看文档</a>
|
||||
<h2>例子:</h2>
|
||||
<ul id="example-list">
|
||||
<li><a href="./search?keywords=海阔天空">搜索</a></li>
|
||||
<li><a href="./comment/music?id=186016&limit=1">歌曲评论</a></li>
|
||||
<li><a href="./dj/program?rid=336355127">电台节目</a></li>
|
||||
<li><a href="./qrlogin.html">二维码登录</a></li>
|
||||
<li><a href="./audio_match_demo/index.html">听歌识曲</a></li>
|
||||
<li><a href="./cloud.html">云盘上传</a></li>
|
||||
<li><a href="./eapi_decrypt.html">eapi 参数/返回值解析</a></li>
|
||||
<li><a href="./api.html">API 调试界面</a></li>
|
||||
<li><a href="./playlist_import.html">歌单导入工具</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<script src="https://fastly.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var origin = window.location.origin;
|
||||
document.getElementById('base-url').textContent = origin;
|
||||
document.getElementById('current-url').textContent = window.location.href;
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const currentUrlElement = document.getElementById('current-url');
|
||||
currentUrlElement.textContent = window.location.href;
|
||||
|
||||
fetch('/inner/version', { method: 'POST' })
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (data) {
|
||||
var v = data && data.data && data.data.version;
|
||||
if (v) document.getElementById('api-version').textContent = 'v' + v;
|
||||
var pre = document.querySelector('pre code');
|
||||
if (pre) pre.textContent = pre.textContent.replace(/\{origin\}/g, origin);
|
||||
})
|
||||
.catch(function () {});
|
||||
const exampleList = document.getElementById('example-list');
|
||||
const items = exampleList.getElementsByTagName('li');
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const number = document.createElement('span');
|
||||
number.className = 'number';
|
||||
number.textContent = (i + 1) + '. ';
|
||||
items[i].prepend(number);
|
||||
}
|
||||
|
||||
// 获取API版本号
|
||||
axios({
|
||||
url: '/inner/version',
|
||||
method: 'post',
|
||||
data: {},
|
||||
}).then((res) => {
|
||||
const body = res.data;
|
||||
const version = body.data.version;
|
||||
const apiVersionElement = document.getElementById('api-version');
|
||||
apiVersionElement.textContent = ` v${version}`;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,481 +1,159 @@
|
||||
<!-- eslint-disable prettier/prettier -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>一起听 - 主机模式</title>
|
||||
<script src="https://unpkg.com/petite-vue"></script>
|
||||
<script src="https://fastly.jsdelivr.net/npm/[email protected]/dist/axios.min.js"></script>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 32px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.login-link {
|
||||
display: block;
|
||||
margin-bottom: 24px;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.login-link:hover {
|
||||
color: #333;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.message {
|
||||
padding: 12px 16px;
|
||||
background: #f9f9f9;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 20px;
|
||||
font-size: 14px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.audio-player {
|
||||
width: 100%;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
background: #333;
|
||||
color: white;
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
margin-right: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 24px;
|
||||
padding: 20px;
|
||||
background: #f9f9f9;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.section h3 {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
input[type="text"], input[type="number"] {
|
||||
padding: 10px 14px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
border-color: #333;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.input-group label {
|
||||
font-size: 14px;
|
||||
color: #555;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.share-link {
|
||||
padding: 12px;
|
||||
background: #f0f0f0;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
word-break: break-all;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.user-list {
|
||||
list-style: none;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.user-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.user-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.track-list {
|
||||
list-style: none;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.track-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.track-item:hover {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.track-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.track-cover {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 4px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.track-name {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
details {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
details summary {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #555;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
details summary:hover {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
details[open] summary {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.control-buttons {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="container">
|
||||
<a href="/qrlogin.html" class="login-link">还没登录?点击登录</a>
|
||||
|
||||
<h1>一起听 - 主机模式</h1>
|
||||
|
||||
<div class="message">消息: {{message}}</div>
|
||||
|
||||
<audio id="player" class="audio-player" autoplay controls></audio>
|
||||
|
||||
<div v-if="!account.login">
|
||||
<button class="btn" @click="login">获取登录状态</button>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>账号信息</h3>
|
||||
<div>当前登录账号: {{account.nickname}}</div>
|
||||
</div>
|
||||
|
||||
<div v-if="account.login" class="section">
|
||||
<h3>房间管理</h3>
|
||||
<div class="control-buttons">
|
||||
<button v-if="!roomInfo.roomId" class="btn" @click="createRoom">创建房间</button>
|
||||
</div>
|
||||
|
||||
<details>
|
||||
<summary>加入房间</summary>
|
||||
<div class="input-group">
|
||||
<label>房间ID:</label>
|
||||
<input v-model="roomInfo.roomId" type="text" />
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<label>邀请者ID:</label>
|
||||
<input v-model="roomInfo.inviterId" type="text" />
|
||||
</div>
|
||||
<button class="btn" @click="joinRoom">加入房间</button>
|
||||
</details>
|
||||
|
||||
<div v-if="roomInfo.roomId" style="margin-top: 16px;">
|
||||
<h4>分享链接</h4>
|
||||
<div class="share-link">
|
||||
https://st.music.163.com/listen-together/share/?songId=1372188635&roomId={{roomInfo.roomId}}&inviterId={{roomInfo.inviterId}}
|
||||
</div>
|
||||
<button class="btn" @click="refreshRoom">刷新房间状态</button>
|
||||
<button class="btn" @click="closeRoom">关闭房间</button>
|
||||
|
||||
<h4 style="margin-top: 16px;">在线用户</h4>
|
||||
<ul class="user-list">
|
||||
<li v-for="user in roomInfo.roomUsers" :key="user.userId" class="user-item">
|
||||
<img :src="user.avatarUrl" class="user-avatar" alt="avatar" />
|
||||
<span class="user-name">{{user.nickname}}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>播放控制</h3>
|
||||
<div class="control-buttons">
|
||||
<button class="btn" @click="playTrack">播放</button>
|
||||
<button class="btn" @click="pauseTrack">暂停</button>
|
||||
<button class="btn" @click="seekTrack">同步进度</button>
|
||||
</div>
|
||||
</div>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>一起听</title>
|
||||
<script src="https://unpkg.com/petite-vue"></script>
|
||||
<script src="https://fastly.jsdelivr.net/npm/[email protected]/dist/axios.min.js"></script>
|
||||
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/css/mdui.min.css" />
|
||||
<script src="https://unpkg.com/[email protected]/dist/js/mdui.min.js"></script>
|
||||
</head>
|
||||
|
||||
<body class="mdui-container">
|
||||
<div>
|
||||
<a href="/qrlogin.html">
|
||||
如果没登录,请先登录
|
||||
</a>
|
||||
</div>
|
||||
<h1>一起听 - 主机模式</h1>
|
||||
<div>消息: {{message}}</div>
|
||||
<audio id="player" autoplay controls>
|
||||
</audio>
|
||||
<br />
|
||||
<br />
|
||||
<button v-if="!account.login" @click="login">获取登录状态</button>
|
||||
<div>您的当前登录账号为: {{account.nickname}}</div>
|
||||
<br />
|
||||
<div v-if="account.login">
|
||||
<button v-if="!roomInfo.roomId" @click="createRoom">创建房间</button>
|
||||
<details>
|
||||
<summary>播放列表</summary>
|
||||
<div class="section">
|
||||
<div class="input-group">
|
||||
<label>歌单ID:</label>
|
||||
<input v-model="playlistInfo.playlistId" type="text" />
|
||||
</div>
|
||||
<button class="btn" @click="loadPlaylist">加载歌单</button>
|
||||
<div style="margin-top: 12px; font-size: 14px; color: #555;">
|
||||
歌单名称: {{playlistInfo.playlistName}}
|
||||
</div>
|
||||
|
||||
<h4 style="margin-top: 16px;">歌单内容</h4>
|
||||
<ul class="track-list">
|
||||
<li
|
||||
@click="gotoTrack(track.id)"
|
||||
v-for="track in playlistInfo.playlistTracks"
|
||||
:key="track.id"
|
||||
class="track-item"
|
||||
>
|
||||
<img :src="track.al.picUrl" class="track-cover" alt="cover" />
|
||||
<span class="track-name">{{track.name}}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<summary>加入房间</summary>
|
||||
<div><span>房间ID: </span><input v-model="roomInfo.roomId" /></div>
|
||||
<div><span>邀请者 ID: </span><input v-model="roomInfo.inviterId" /></div>
|
||||
<button @click="joinRoom">点击加入</button>
|
||||
</details>
|
||||
</body>
|
||||
|
||||
<script>
|
||||
PetiteVue.createApp({
|
||||
message: '请点击获取登录状态',
|
||||
account: {
|
||||
login: false,
|
||||
userId: 0,
|
||||
nickname: '未登录',
|
||||
},
|
||||
roomInfo: {
|
||||
roomId: null,
|
||||
inviterId: 0,
|
||||
roomUsers: [],
|
||||
},
|
||||
playlistInfo: {
|
||||
playlistId: 0,
|
||||
playlistName: '未获取',
|
||||
playlistTrackIds: [],
|
||||
playlistTracks: [],
|
||||
},
|
||||
playingInfo: {
|
||||
trackId: 0,
|
||||
status: 'PLAY',
|
||||
progress: 1,
|
||||
},
|
||||
clientSeq: 1,
|
||||
login: async function () {
|
||||
const res = await axios({
|
||||
url: `/login/status?timestamp=${new Date().getTime()}`,
|
||||
method: 'get',
|
||||
data: {
|
||||
cookie: localStorage.getItem('cookie'),
|
||||
},
|
||||
})
|
||||
if (res.data.data.code != 200) {
|
||||
alert('请先使用登录 API 登录到网易云音乐')
|
||||
} else {
|
||||
this.account.userId = res.data.data.profile.userId
|
||||
this.account.nickname = res.data.data.profile.nickname
|
||||
this.account.login = true
|
||||
this.message = '成功登录,请创建房间'
|
||||
}
|
||||
},
|
||||
joinRoom: async function () {
|
||||
const res = await axios({
|
||||
url: 'listentogether/accept',
|
||||
method: 'post',
|
||||
data: {
|
||||
roomId: this.roomInfo.roomId,
|
||||
inviterId: this.roomInfo.inviterId,
|
||||
cookie: localStorage.getItem('cookie'),
|
||||
},
|
||||
})
|
||||
console.info(res)
|
||||
if (res.data.code != 200) {
|
||||
this.message = '加入房间出现问题: ' + res.data.message
|
||||
} else {
|
||||
this.message = '加入房间成功: ' + this.roomInfo.roomId
|
||||
const res2 = await axios({
|
||||
url: 'listentogether/room/check',
|
||||
method: 'post',
|
||||
data: {
|
||||
roomId: this.roomInfo.roomId,
|
||||
cookie: localStorage.getItem('cookie'),
|
||||
},
|
||||
})
|
||||
console.info(res2)
|
||||
const res3 = await axios({
|
||||
url: 'listentogether/sync/playlist/get',
|
||||
method: 'post',
|
||||
data: {
|
||||
roomId: this.roomInfo.roomId,
|
||||
cookie: localStorage.getItem('cookie'),
|
||||
},
|
||||
})
|
||||
<div v-if="roomInfo.roomId">
|
||||
<div>分享链接为:
|
||||
https://st.music.163.com/listen-together/share/?songId=1372188635&roomId={{roomInfo.roomId}}&inviterId={{roomInfo.inviterId}}
|
||||
</div>
|
||||
<br />
|
||||
<button @click="refreshRoom">刷新房间状态</button>
|
||||
<div>在线用户: </div>
|
||||
<ul class="mdui-list">
|
||||
<li v-for="user in roomInfo.roomUsers" class="mdui-list-item mdui-ripple">
|
||||
<div class="mdui-list-item-avatar">
|
||||
<img :src="user.avatarUrl" />
|
||||
</div>
|
||||
<div class="mdui-list-item-content">{{user.nickname}}</div>
|
||||
</li>
|
||||
</ul>
|
||||
<button v-if="roomInfo.roomId" @click="closeRoom">关闭房间</button>
|
||||
</div>
|
||||
</div>
|
||||
<button @click="playTrack">播放</button>
|
||||
<button @click="pauseTrack">暂停</button>
|
||||
<button @click="seekTrack">同步进度</button>
|
||||
<details>
|
||||
<summary>播放列表</summary>
|
||||
<br />
|
||||
<div><span>歌单ID: </span><input v-model="playlistInfo.playlistId" /></div>
|
||||
<button @click="loadPlaylist">加载歌单到播放列表</button>
|
||||
<span>{{playlistInfo.playlistName}}</span>
|
||||
<br />
|
||||
<br />
|
||||
<div>歌单内容: </div>
|
||||
<ul class="mdui-list">
|
||||
<li @click="gotoTrack(track.id)" v-for="track in playlistInfo.playlistTracks" class="mdui-list-item mdui-ripple">
|
||||
<div class="mdui-list-item-avatar">
|
||||
<img :src="track.al.picUrl" />
|
||||
</div>
|
||||
<div class="mdui-list-item-content">{{track.name}}</div>
|
||||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
|
||||
this.playlistInfo.playlistName = '其他人的歌单'
|
||||
this.playlistInfo.playlistTrackIds =
|
||||
res3.data.data.playlist.displayList.result.join(',')
|
||||
const resa = await axios({
|
||||
url: '/song/detail',
|
||||
method: 'post',
|
||||
data: {
|
||||
ids: this.playlistInfo.playlistTrackIds,
|
||||
cookie: localStorage.getItem('cookie'),
|
||||
},
|
||||
})
|
||||
console.info(resa)
|
||||
this.playlistInfo.playlistTracks = resa.data.songs
|
||||
}
|
||||
},
|
||||
createRoom: async function () {
|
||||
const res = await axios({
|
||||
url: 'listentogether/room/create',
|
||||
method: 'get',
|
||||
data: {
|
||||
cookie: localStorage.getItem('cookie'),
|
||||
},
|
||||
})
|
||||
console.info(res)
|
||||
if (res.data.code != 200) {
|
||||
this.message = '创建房间出现问题: ' + res.data.message
|
||||
} else {
|
||||
this.message = '创建房间成功: ' + res.data.data.roomInfo.roomId
|
||||
this.roomInfo.inviterId = this.account.userId
|
||||
this.roomInfo.roomId = res.data.data.roomInfo.roomId
|
||||
const res2 = await axios({
|
||||
url: 'listentogether/room/check',
|
||||
method: 'post',
|
||||
data: {
|
||||
roomId: this.roomInfo.roomId,
|
||||
cookie: localStorage.getItem('cookie'),
|
||||
},
|
||||
})
|
||||
console.info(res2)
|
||||
}
|
||||
},
|
||||
refreshRoom: async function () {
|
||||
const res = await axios({
|
||||
url: '/listentogether/status',
|
||||
data: {
|
||||
cookie: localStorage.getItem('cookie'),
|
||||
},
|
||||
})
|
||||
console.info(res)
|
||||
if (res.data.code != 200 || !res.data.data.inRoom) {
|
||||
this.message = '房间状态获取失败,可能退出了房间'
|
||||
} else {
|
||||
this.roomInfo.roomUsers = res.data.data.roomInfo.roomUsers
|
||||
}
|
||||
},
|
||||
closeRoom: async function () {
|
||||
const res = await axios({
|
||||
url: '/listentogether/end',
|
||||
</body>
|
||||
<script>
|
||||
PetiteVue.createApp({
|
||||
message: '请点击获取登录状态',
|
||||
account: {
|
||||
login: false,
|
||||
userId: 0,
|
||||
nickname: '未登录',
|
||||
},
|
||||
roomInfo: {
|
||||
roomId: null,
|
||||
inviterId: 0,
|
||||
roomUsers: [],
|
||||
},
|
||||
playlistInfo: {
|
||||
playlistId: 0,
|
||||
playlistName: '未获取',
|
||||
playlistTrackIds: [],
|
||||
playlistTracks: [],
|
||||
},
|
||||
playingInfo: {
|
||||
trackId: 0,
|
||||
status: 'PLAY',
|
||||
progress: 1,
|
||||
},
|
||||
clientSeq: 1,
|
||||
login: async function () {
|
||||
const res = await axios({
|
||||
url: `/login/status?timestamp=${new Date().getTime()}`,
|
||||
method: 'get',
|
||||
data: {
|
||||
cookie: localStorage.getItem('cookie'),
|
||||
},
|
||||
})
|
||||
if (res.data.data.code != 200) {
|
||||
alert('请先使用登录 API 登录到网易云音乐')
|
||||
} else {
|
||||
this.account.userId = res.data.data.profile.userId
|
||||
this.account.nickname = res.data.data.profile.nickname
|
||||
this.account.login = true
|
||||
this.message = '成功登录, 请创建房间'
|
||||
}
|
||||
},
|
||||
joinRoom: async function () {
|
||||
const res = await axios({
|
||||
url: 'listentogether/accept',
|
||||
method: 'post',
|
||||
data: {
|
||||
roomId: this.roomInfo.roomId,
|
||||
inviterId: this.roomInfo.inviterId,
|
||||
cookie: localStorage.getItem('cookie'),
|
||||
},
|
||||
})
|
||||
console.log(res)
|
||||
if (res.data.code != 200) {
|
||||
this.message = '加入房间出现问题: ' + res.data.message
|
||||
} else {
|
||||
this.message = '加入房间成功: ' + this.roomInfo.roomId
|
||||
const res2 = await axios({
|
||||
url: 'listentogether/room/check',
|
||||
method: 'post',
|
||||
data: {
|
||||
roomId: this.roomInfo.roomId,
|
||||
cookie: localStorage.getItem('cookie'),
|
||||
},
|
||||
})
|
||||
console.info(res)
|
||||
if (res.data.code != 200 || !res.data.data.success) {
|
||||
this.message = '房间关闭失败'
|
||||
} else {
|
||||
this.message = '房间关闭成功'
|
||||
this.roomInfo.roomId = null
|
||||
}
|
||||
},
|
||||
loadPlaylist: async function () {
|
||||
const res = await axios({
|
||||
url: '/playlist/detail',
|
||||
console.log(res2)
|
||||
const res3 = await axios({
|
||||
url: 'listentogether/sync/playlist/get',
|
||||
method: 'post',
|
||||
data: {
|
||||
id: this.playlistInfo.playlistId,
|
||||
roomId: this.roomInfo.roomId,
|
||||
cookie: localStorage.getItem('cookie'),
|
||||
},
|
||||
})
|
||||
console.info(res)
|
||||
this.playlistInfo.playlistName = res.data.playlist.name
|
||||
this.playlistInfo.playlistTrackIds = res.data.playlist.trackIds
|
||||
.map((track) => track.id)
|
||||
|
||||
this.playlistInfo.playlistName = "其他人的歌单"
|
||||
this.playlistInfo.playlistTrackIds = res3.data.data.playlist.displayList.result
|
||||
.join(',')
|
||||
const resa = await axios({
|
||||
url: '/song/detail',
|
||||
@@ -485,68 +163,156 @@
|
||||
cookie: localStorage.getItem('cookie'),
|
||||
},
|
||||
})
|
||||
console.info(resa)
|
||||
console.log(resa)
|
||||
this.playlistInfo.playlistTracks = resa.data.songs
|
||||
if (this.roomInfo.roomId) {
|
||||
const resb = await axios({
|
||||
url: 'listentogether/sync/list/command',
|
||||
method: 'post',
|
||||
data: {
|
||||
roomId: this.roomInfo.roomId,
|
||||
commandType: 'REPLACE',
|
||||
userId: this.account.userId,
|
||||
version: this.clientSeq++,
|
||||
playMode: 'ORDER_LOOP',
|
||||
displayList: this.playlistInfo.playlistTrackIds,
|
||||
randomList: this.playlistInfo.playlistTrackIds,
|
||||
cookie: localStorage.getItem('cookie'),
|
||||
},
|
||||
})
|
||||
console.info(resb)
|
||||
}
|
||||
},
|
||||
gotoTrack: async function (trackId) {
|
||||
this.playingInfo.trackId = trackId
|
||||
if (this.roomInfo.roomId) {
|
||||
await this.playCommand('GOTO')
|
||||
}
|
||||
document.getElementById('player').src =
|
||||
'https://music.163.com/song/media/outer/url?id=' + trackId + '.mp3'
|
||||
},
|
||||
playTrack: async function () {
|
||||
this.playingInfo.status = 'PLAY'
|
||||
await this.playCommand('PLAY')
|
||||
document.getElementById('player').play()
|
||||
},
|
||||
pauseTrack: async function () {
|
||||
this.playingInfo.status = 'PAUSE'
|
||||
await this.playCommand('PAUSE')
|
||||
document.getElementById('player').pause()
|
||||
},
|
||||
seekTrack: async function () {
|
||||
this.playingInfo.status = 'PLAY'
|
||||
await this.playCommand('seek')
|
||||
document.getElementById('player').play()
|
||||
},
|
||||
playCommand: async function (action) {
|
||||
const res = await axios({
|
||||
url: 'listentogether/play/command',
|
||||
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
createRoom: async function () {
|
||||
const res = await axios({
|
||||
url: 'listentogether/room/create',
|
||||
method: 'get',
|
||||
data: {
|
||||
cookie: localStorage.getItem('cookie'),
|
||||
},
|
||||
})
|
||||
console.log(res)
|
||||
if (res.data.code != 200) {
|
||||
this.message = '创建房间出现问题: ' + res.data.message
|
||||
} else {
|
||||
this.message = '创建房间成功: ' + res.data.data.roomInfo.roomId
|
||||
this.roomInfo.inviterId = this.account.userId
|
||||
this.roomInfo.roomId = res.data.data.roomInfo.roomId
|
||||
const res2 = await axios({
|
||||
url: 'listentogether/room/check',
|
||||
method: 'post',
|
||||
data: {
|
||||
roomId: this.roomInfo.roomId,
|
||||
progress: Math.floor(
|
||||
document.getElementById('player').currentTime * 1000,
|
||||
),
|
||||
commandType: action,
|
||||
formerSongId: '-1',
|
||||
targetSongId: this.playingInfo.trackId,
|
||||
clientSeq: this.clientSeq++,
|
||||
playStatus: this.playingInfo.status,
|
||||
cookie: localStorage.getItem('cookie'),
|
||||
},
|
||||
})
|
||||
console.info(res)
|
||||
},
|
||||
}).mount()
|
||||
</script>
|
||||
console.log(res2)
|
||||
}
|
||||
},
|
||||
refreshRoom: async function () {
|
||||
const res = await axios({
|
||||
url: '/listentogether/status',
|
||||
data: {
|
||||
cookie: localStorage.getItem('cookie'),
|
||||
},
|
||||
})
|
||||
console.log(res)
|
||||
if (res.data.code != 200 || !res.data.data.inRoom) {
|
||||
this.message = '房间状态获取失败, 可能退出了房间'
|
||||
} else {
|
||||
this.roomInfo.roomUsers = res.data.data.roomInfo.roomUsers
|
||||
}
|
||||
},
|
||||
closeRoom: async function () {
|
||||
const res = await axios({
|
||||
url: '/listentogether/end',
|
||||
method: 'post',
|
||||
data: {
|
||||
roomId: this.roomInfo.roomId,
|
||||
cookie: localStorage.getItem('cookie'),
|
||||
|
||||
},
|
||||
})
|
||||
console.log(res)
|
||||
if (res.data.code != 200 || !res.data.data.success) {
|
||||
this.message = '房间关闭失败'
|
||||
} else {
|
||||
this.message = '房间关闭成功'
|
||||
this.roomInfo.roomId = null
|
||||
}
|
||||
},
|
||||
loadPlaylist: async function () {
|
||||
const res = await axios({
|
||||
url: '/playlist/detail',
|
||||
method: 'post',
|
||||
data: {
|
||||
id: this.playlistInfo.playlistId,
|
||||
cookie: localStorage.getItem('cookie'),
|
||||
},
|
||||
})
|
||||
console.log(res)
|
||||
this.playlistInfo.playlistName = res.data.playlist.name
|
||||
this.playlistInfo.playlistTrackIds = res.data.playlist.trackIds
|
||||
.map((track) => track.id)
|
||||
.join(',')
|
||||
const resa = await axios({
|
||||
url: '/song/detail',
|
||||
method: 'post',
|
||||
data: {
|
||||
ids: this.playlistInfo.playlistTrackIds,
|
||||
cookie: localStorage.getItem('cookie'),
|
||||
},
|
||||
})
|
||||
console.log(resa)
|
||||
this.playlistInfo.playlistTracks = resa.data.songs
|
||||
if (this.roomInfo.roomId) {
|
||||
const resb = await axios({
|
||||
url: 'listentogether/sync/list/command',
|
||||
method: 'post',
|
||||
data: {
|
||||
roomId: this.roomInfo.roomId,
|
||||
commandType: 'REPLACE',
|
||||
userId: this.account.userId,
|
||||
version: this.clientSeq++,
|
||||
playMode: 'ORDER_LOOP',
|
||||
displayList: this.playlistInfo.playlistTrackIds,
|
||||
randomList: this.playlistInfo.playlistTrackIds,
|
||||
cookie: localStorage.getItem('cookie'),
|
||||
},
|
||||
})
|
||||
console.log(resb)
|
||||
}
|
||||
},
|
||||
gotoTrack: async function (trackId) {
|
||||
this.playingInfo.trackId = trackId
|
||||
if (this.roomInfo.roomId) {
|
||||
await this.playCommand('GOTO')
|
||||
}
|
||||
document.getElementById('player').src =
|
||||
'https://music.163.com/song/media/outer/url?id=' + trackId + '.mp3'
|
||||
},
|
||||
playTrack: async function () {
|
||||
this.playingInfo.status = 'PLAY'
|
||||
await this.playCommand('PLAY')
|
||||
document.getElementById('player').play()
|
||||
},
|
||||
pauseTrack: async function () {
|
||||
this.playingInfo.status = 'PAUSE'
|
||||
await this.playCommand('PAUSE')
|
||||
document.getElementById('player').pause()
|
||||
},
|
||||
seekTrack: async function () {
|
||||
this.playingInfo.status = 'PLAY'
|
||||
await this.playCommand('seek')
|
||||
document.getElementById('player').play()
|
||||
},
|
||||
playCommand: async function (action) {
|
||||
const res = await axios({
|
||||
url: 'listentogether/play/command',
|
||||
method: 'post',
|
||||
data: {
|
||||
roomId: this.roomInfo.roomId,
|
||||
progress: Math.floor(
|
||||
document.getElementById('player').currentTime * 1000,
|
||||
),
|
||||
commandType: action,
|
||||
formerSongId: '-1',
|
||||
targetSongId: this.playingInfo.trackId,
|
||||
clientSeq: this.clientSeq++,
|
||||
playStatus: this.playingInfo.status,
|
||||
cookie: localStorage.getItem('cookie'),
|
||||
},
|
||||
})
|
||||
console.log(res)
|
||||
},
|
||||
}).mount()
|
||||
</script>
|
||||
|
||||
</html>
|
||||
|
||||
@@ -5,143 +5,21 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>登录</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 40px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
max-width: 400px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #555;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
font-size: 15px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
border-color: #333;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
padding: 14px;
|
||||
background: #333;
|
||||
color: white;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
background: #999;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.result {
|
||||
margin-top: 24px;
|
||||
padding: 16px;
|
||||
background: #f9f9f9;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
text-align: left;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #ef4444;
|
||||
background: #fef2f2;
|
||||
}
|
||||
|
||||
.success {
|
||||
color: #10b981;
|
||||
background: #f0fdf4;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>登录</h1>
|
||||
<p class="subtitle">使用手机号和密码登录网易云音乐</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="phone">手机号</label>
|
||||
<input type="tel" id="phone" placeholder="请输入手机号" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">密码</label>
|
||||
<input type="password" id="password" placeholder="请输入密码" />
|
||||
</div>
|
||||
|
||||
<button id="loginBtn" class="btn" onclick="handleLogin()">登录</button>
|
||||
|
||||
<div id="result" class="result" style="display: none;"></div>
|
||||
</div>
|
||||
|
||||
<script src="https://fastly.jsdelivr.net/npm/[email protected]/dist/axios.min.js"></script>
|
||||
<script>
|
||||
const phone = '' // 这里填手机号
|
||||
const password = '' // 这里填密码
|
||||
const fileUpdateTime = {}
|
||||
if (!phone || !password) {
|
||||
const msg = '请设置你的手机号码和密码'
|
||||
alert(msg)
|
||||
throw new Error(msg)
|
||||
}
|
||||
|
||||
async function login(phone, password) {
|
||||
async function login() {
|
||||
const res = await axios({
|
||||
url: `/login/cellphone`,
|
||||
method: 'post',
|
||||
@@ -152,67 +30,17 @@
|
||||
})
|
||||
return res.data.cookie
|
||||
}
|
||||
|
||||
async function handleLogin() {
|
||||
const phoneInput = document.getElementById('phone')
|
||||
const passwordInput = document.getElementById('password')
|
||||
const loginBtn = document.getElementById('loginBtn')
|
||||
const resultDiv = document.getElementById('result')
|
||||
|
||||
const phone = phoneInput.value.trim()
|
||||
const password = passwordInput.value
|
||||
|
||||
if (!phone || !password) {
|
||||
showResult('请输入手机号和密码', 'error')
|
||||
return
|
||||
}
|
||||
|
||||
loginBtn.disabled = true
|
||||
loginBtn.textContent = '登录中...'
|
||||
showResult('正在登录...', 'info')
|
||||
|
||||
try {
|
||||
const cookieToken = await login(phone, password)
|
||||
localStorage.setItem('cookie', cookieToken)
|
||||
|
||||
const res = await axios({
|
||||
url: `/login/status`,
|
||||
method: 'post',
|
||||
data: {
|
||||
cookie: cookieToken,
|
||||
},
|
||||
})
|
||||
|
||||
showResult(`登录成功!\n${JSON.stringify(res.data, null, 2)}`, 'success')
|
||||
} catch (error) {
|
||||
console.error('登录失败:', error)
|
||||
const errorMsg = error.response?.data?.message || error.message || '登录失败,请重试'
|
||||
showResult(`登录失败:${errorMsg}`, 'error')
|
||||
} finally {
|
||||
loginBtn.disabled = false
|
||||
loginBtn.textContent = '登录'
|
||||
}
|
||||
async function main() {
|
||||
const cookieToken = await login()
|
||||
const res = await axios({
|
||||
url: `/login/status`,
|
||||
method: 'post',
|
||||
data: {
|
||||
cookie: cookieToken,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function showResult(message, type = 'info') {
|
||||
const resultDiv = document.getElementById('result')
|
||||
resultDiv.style.display = 'block'
|
||||
resultDiv.textContent = message
|
||||
resultDiv.className = 'result ' + type
|
||||
}
|
||||
|
||||
// 支持回车登录
|
||||
document.getElementById('password').addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
handleLogin()
|
||||
}
|
||||
})
|
||||
|
||||
document.getElementById('phone').addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
document.getElementById('password').focus()
|
||||
}
|
||||
})
|
||||
main()
|
||||
</script>
|
||||
</body>
|
||||
|
||||
|
||||
@@ -5,297 +5,56 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>歌单封面上传</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 32px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.login-link {
|
||||
display: block;
|
||||
margin-bottom: 24px;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.login-link:hover {
|
||||
color: #333;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.cover-wrapper {
|
||||
position: relative;
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
margin: 0 auto 24px;
|
||||
}
|
||||
|
||||
.cover {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
border: 4px solid #fff;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #555;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
input[type="text"] {
|
||||
width: 100%;
|
||||
padding: 10px 14px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
input[type="text"]:focus {
|
||||
border-color: #333;
|
||||
}
|
||||
|
||||
.upload-btn {
|
||||
display: inline-block;
|
||||
padding: 12px 28px;
|
||||
background: #333;
|
||||
color: white;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.upload-btn:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
.upload-btn input[type="file"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.loading {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.loading.active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 4px solid #e0e0e0;
|
||||
border-top-color: #333;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.result {
|
||||
margin-top: 20px;
|
||||
padding: 12px 16px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.result.success {
|
||||
background: #d1fae5;
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
.result.error {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>歌单封面上传</h1>
|
||||
<p class="subtitle">上传自定义歌单封面图片</p>
|
||||
|
||||
<a href="/qrlogin-nocookie.html" class="login-link">还没登录?点击登录</a>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="playlistId">歌单 ID</label>
|
||||
<input type="text" id="playlistId" placeholder="请输入歌单ID" />
|
||||
</div>
|
||||
|
||||
<div class="cover-wrapper">
|
||||
<img id="playlist_cover" class="cover" src="" alt="歌单封面" />
|
||||
<div class="loading" id="loading">
|
||||
<div class="spinner"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label class="upload-btn">
|
||||
选择封面图片
|
||||
<input id="file" type="file" name="filename" accept="image/*" />
|
||||
</label>
|
||||
|
||||
<div id="result" class="result" style="display: none;"></div>
|
||||
<div>
|
||||
<a href="/qrlogin-nocookie.html">
|
||||
如果没登录,请先登录
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<input id="file" type="file" name="filename" />
|
||||
<img id="playlist_cover" style="height: 200px; width: 200px; border-radius: 50%" />
|
||||
<script src="https://fastly.jsdelivr.net/npm/[email protected]/dist/axios.min.js"></script>
|
||||
<script>
|
||||
const loadingOverlay = document.getElementById('loading')
|
||||
const playlistIdInput = document.getElementById('playlistId')
|
||||
const resultDiv = document.getElementById('result')
|
||||
|
||||
function showLoading() {
|
||||
loadingOverlay.classList.add('active')
|
||||
const playlist_id = ''
|
||||
if (!playlist_id) {
|
||||
const msg = '请设置你的歌单id'
|
||||
alert(msg)
|
||||
throw new Error(msg)
|
||||
}
|
||||
|
||||
function hideLoading() {
|
||||
loadingOverlay.classList.remove('active')
|
||||
}
|
||||
|
||||
function showResult(message, type) {
|
||||
resultDiv.textContent = message
|
||||
resultDiv.className = 'result ' + type
|
||||
resultDiv.style.display = 'block'
|
||||
}
|
||||
|
||||
function hideResult() {
|
||||
resultDiv.style.display = 'none'
|
||||
}
|
||||
|
||||
async function loadPlaylistCover() {
|
||||
const playlistId = playlistIdInput.value.trim()
|
||||
if (!playlistId) {
|
||||
return
|
||||
}
|
||||
|
||||
showLoading()
|
||||
hideResult()
|
||||
|
||||
try {
|
||||
const res = await axios({
|
||||
url: `/playlist/detail?id=${playlistId}×tamp=${Date.now()}`,
|
||||
})
|
||||
document.querySelector('#playlist_cover').src = res.data.playlist.coverImgUrl
|
||||
hideResult()
|
||||
} catch (error) {
|
||||
console.error('加载封面失败:', error)
|
||||
showResult('加载封面失败,请检查歌单ID', 'error')
|
||||
} finally {
|
||||
hideLoading()
|
||||
}
|
||||
}
|
||||
|
||||
// 监听歌单ID输入变化
|
||||
playlistIdInput.addEventListener('input', function() {
|
||||
loadPlaylistCover()
|
||||
})
|
||||
|
||||
// 监听文件选择
|
||||
document
|
||||
.querySelector('input[type="file"]')
|
||||
.addEventListener('change', async function (e) {
|
||||
const file = this.files[0]
|
||||
const playlistId = playlistIdInput.value.trim()
|
||||
|
||||
if (!playlistId) {
|
||||
showResult('请先输入歌单ID', 'error')
|
||||
return
|
||||
}
|
||||
|
||||
if (!file) {
|
||||
return
|
||||
}
|
||||
|
||||
showLoading()
|
||||
hideResult()
|
||||
|
||||
try {
|
||||
await upload(file, playlistId)
|
||||
} catch (error) {
|
||||
console.error('上传失败:', error)
|
||||
showResult('上传失败,请重试', 'error')
|
||||
} finally {
|
||||
hideLoading()
|
||||
}
|
||||
main()
|
||||
async function main() {
|
||||
document.querySelector('input[type="file"]').addEventListener(
|
||||
'change',
|
||||
function (e) {
|
||||
var file = this.files[0]
|
||||
upload(file)
|
||||
},
|
||||
false,
|
||||
)
|
||||
const res = await axios({
|
||||
url: `/playlist/detail?id=${playlist_id}×tamp=${Date.now()}`,
|
||||
})
|
||||
document.querySelector('#playlist_cover').src = res.data.playlist.coverImgUrl
|
||||
}
|
||||
|
||||
async function upload(file, playlistId) {
|
||||
async function upload(file) {
|
||||
var formData = new FormData()
|
||||
formData.append('imgFile', file)
|
||||
const imgSize = await getImgSize(file)
|
||||
const res = await axios({
|
||||
method: 'post',
|
||||
url: `/playlist/cover/update?id=${playlistId}&cookie=${localStorage.getItem('cookie')}&imgSize=${imgSize.width}&imgX=0&imgY=0×tamp=${Date.now()}`,
|
||||
url: `/playlist/cover/update?id=${playlist_id}&cookie=${localStorage.getItem('cookie')}&imgSize=${imgSize.width
|
||||
}&imgX=0&imgY=0×tamp=${Date.now()}`,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
data: formData,
|
||||
})
|
||||
document.querySelector('#playlist_cover').src = res.data.data.url
|
||||
showResult('封面上传成功!', 'success')
|
||||
}
|
||||
|
||||
function getImgSize(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let reader = new FileReader()
|
||||
@@ -309,16 +68,10 @@
|
||||
height: this.height,
|
||||
})
|
||||
}
|
||||
image.onerror = function() {
|
||||
reject(new Error('图片加载失败'))
|
||||
}
|
||||
}
|
||||
reader.onerror = function() {
|
||||
reject(new Error('文件读取失败'))
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -2,416 +2,261 @@
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>歌单导入工具</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 32px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
margin-bottom: 24px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
padding: 12px 24px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.tab-btn:hover {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.tab-btn.active {
|
||||
color: #333;
|
||||
border-bottom-color: #333;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-content.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #555;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
input[type="text"], textarea {
|
||||
width: 100%;
|
||||
padding: 10px 14px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
input[type="text"]:focus, textarea:focus {
|
||||
border-color: #333;
|
||||
}
|
||||
|
||||
textarea {
|
||||
min-height: 120px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
table th, table td {
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
table th {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #555;
|
||||
background: #f9f9f9;
|
||||
}
|
||||
|
||||
table td input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 10px 20px;
|
||||
background: #333;
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #666;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #888;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.input-group input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.checkbox-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.checkbox-group input[type="checkbox"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.checkbox-group label {
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input:disabled {
|
||||
background: #f5f5f5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
<!-- 引入Bootstrap CSS -->
|
||||
<link href="https://fastly.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<!-- 引入Bootstrap JS -->
|
||||
<script src="https://fastly.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<!-- 引入axios用于发送异步请求 -->
|
||||
<script src="https://fastly.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>歌单导入工具</h1>
|
||||
<p class="subtitle">请选择一种导入方式并填写相关信息</p>
|
||||
<div class="container mt-5">
|
||||
<h1 class="mb-4">歌单导入工具</h1>
|
||||
<p>请选择一种导入方式并填写相关信息:</p>
|
||||
|
||||
<ul class="tabs" id="importTabs" role="tablist">
|
||||
<li role="presentation">
|
||||
<button class="tab-btn active" id="metadata-tab" data-bs-toggle="tab" data-bs-target="#metadata" type="button" role="tab" aria-controls="metadata" aria-selected="true">元数据导入</button>
|
||||
</li>
|
||||
<li role="presentation">
|
||||
<button class="tab-btn" id="text-tab" data-bs-toggle="tab" data-bs-target="#text" type="button" role="tab" aria-controls="text" aria-selected="false">文字导入</button>
|
||||
</li>
|
||||
<li role="presentation">
|
||||
<button class="tab-btn" id="link-tab" data-bs-toggle="tab" data-bs-target="#link" type="button" role="tab" aria-controls="link" aria-selected="false">链接导入</button>
|
||||
</li>
|
||||
</ul>
|
||||
<!-- 表单开始 -->
|
||||
<form id="importForm" novalidate>
|
||||
<!-- 选项卡导航 -->
|
||||
<ul class="nav nav-tabs mb-3" id="importTabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="metadata-tab" data-bs-toggle="tab" data-bs-target="#metadata" type="button" role="tab" aria-controls="metadata" aria-selected="true">元数据导入</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="text-tab" data-bs-toggle="tab" data-bs-target="#text" type="button" role="tab" aria-controls="text" aria-selected="false">文字导入</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="link-tab" data-bs-toggle="tab" data-bs-target="#link" type="button" role="tab" aria-controls="link" aria-selected="false">链接导入</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content active" id="importTabContent">
|
||||
<!-- 元数据导入 -->
|
||||
<div class="tab-content active" id="metadata" role="tabpanel" aria-labelledby="metadata-tab">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 33%">歌曲名称</th>
|
||||
<th style="width: 33%">艺术家</th>
|
||||
<th style="width: 33%">专辑</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="metadataTableBody">
|
||||
<tr>
|
||||
<td><input type="text" name="name[]" placeholder="歌曲名称"></td>
|
||||
<td><input type="text" name="artist[]" placeholder="艺术家"></td>
|
||||
<td><input type="text" name="album[]" placeholder="专辑"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<button type="button" class="btn btn-secondary" id="addMetadataRow">增加歌曲</button>
|
||||
</div>
|
||||
<!-- 文字导入 -->
|
||||
<div class="tab-content" id="text" role="tabpanel" aria-labelledby="text-tab">
|
||||
<div class="form-group">
|
||||
<label for="textInput">文字内容</label>
|
||||
<textarea id="textInput" name="text" rows="5" placeholder="请输入歌曲信息,每行一首歌曲"></textarea>
|
||||
<!-- 选项卡面板 -->
|
||||
<div class="tab-content" id="importTabContent">
|
||||
<!-- 元数据导入 -->
|
||||
<div class="tab-pane fade show active" id="metadata" role="tabpanel" aria-labelledby="metadata-tab">
|
||||
<table class="table table-bordered mb-3">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">歌曲名称</th>
|
||||
<th scope="col">艺术家</th>
|
||||
<th scope="col">专辑</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="metadataTableBody">
|
||||
<!-- 默认添加一行 -->
|
||||
<tr>
|
||||
<td><input type="text" class="form-control" name="name[]" placeholder="歌曲名称"></td>
|
||||
<td><input type="text" class="form-control" name="artist[]" placeholder="艺术家"></td>
|
||||
<td><input type="text" class="form-control" name="album[]" placeholder="专辑"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<button type="button" class="btn btn-secondary mb-3" id="addMetadataRow">增加歌曲</button>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="playlistNameInput">歌单名称</label>
|
||||
<input type="text" id="playlistNameInput" name="playlistName" placeholder="请输入歌单名">
|
||||
<!-- 文字导入 -->
|
||||
<div class="tab-pane fade" id="text" role="tabpanel" aria-labelledby="text-tab">
|
||||
<div class="mb-3">
|
||||
<label for="textInput" class="form-label">文字:</label>
|
||||
<textarea class="form-control" id="textInput" name="text" rows="5"></textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="playlistNameInput" class="form-label">歌单名:</label>
|
||||
<input type="text" class="form-control" id="playlistNameInput" name="playlistName" placeholder="请输入歌单名">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 链接导入 -->
|
||||
<div class="tab-content" id="link" role="tabpanel" aria-labelledby="link-tab">
|
||||
<div class="form-group">
|
||||
<label>链接列表</label>
|
||||
<div id="linkInputsContainer">
|
||||
<div class="input-group">
|
||||
<input type="text" id="linkInput0" name="linkInput0" placeholder="请输入链接">
|
||||
<button type="button" class="btn btn-secondary removeLinkButton" data-index="0">×</button>
|
||||
<!-- 链接导入 -->
|
||||
<div class="tab-pane fade" id="link" role="tabpanel" aria-labelledby="link-tab">
|
||||
<div class="mb-3">
|
||||
<label for="linkInputs" class="form-label">链接:</label>
|
||||
<div id="linkInputsContainer">
|
||||
<div class="input-group mb-3">
|
||||
<input type="text" class="form-control" id="linkInput0" name="linkInput0" placeholder="请输入链接">
|
||||
<button type="button" class="btn btn-secondary removeLinkButton" data-index="0">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-secondary mb-3" id="addLinkButton">增加链接</button>
|
||||
<div class="mb-3">
|
||||
<label for="playlistNameLinkInput" class="form-label">歌单名:</label>
|
||||
<input type="text" class="form-control" id="playlistNameLinkInput" name="playlistName" placeholder="请输入歌单名">
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-secondary" id="addLinkButton" style="margin-top: 8px;">增加链接</button>
|
||||
</div>
|
||||
<div class="form-group" style="margin-top: 20px;">
|
||||
<label for="playlistNameLinkInput">歌单名称</label>
|
||||
<input type="text" id="playlistNameLinkInput" name="playlistName" placeholder="请输入歌单名">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="checkbox-group">
|
||||
<input type="checkbox" value="" id="importStarCheckbox">
|
||||
<label for="importStarCheckbox">
|
||||
导入"我喜欢的音乐"
|
||||
</label>
|
||||
</div>
|
||||
<!-- 是否导入我喜欢的音乐 -->
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" value="" id="importStarCheckbox">
|
||||
<label class="form-check-label" for="importStarCheckbox">
|
||||
导入“我喜欢的音乐”
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn" id="submitBtn">导入歌曲</button>
|
||||
</div>
|
||||
<!-- 提交按钮 -->
|
||||
<button type="submit" class="btn btn-primary mt-3">导入歌曲</button>
|
||||
</form>
|
||||
<!-- 表单结束 -->
|
||||
|
||||
<script src="https://fastly.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||
<script>
|
||||
// 选项卡切换
|
||||
const tabBtns = document.querySelectorAll('.tab-btn');
|
||||
const tabContents = document.querySelectorAll('.tab-content[id]');
|
||||
<script>
|
||||
// 动态增加链接输入框
|
||||
document.getElementById('addLinkButton').addEventListener('click', function() {
|
||||
var container = document.getElementById('linkInputsContainer');
|
||||
var newIndex = container.childElementCount - 1; // 减去非输入框元素的数量
|
||||
var newInput = document.createElement('input');
|
||||
newInput.type = 'text';
|
||||
newInput.className = 'form-control';
|
||||
newInput.id = `linkInput${newIndex}`;
|
||||
newInput.name = `linkInput${newIndex}`;
|
||||
newInput.placeholder = '请输入链接';
|
||||
|
||||
tabBtns.forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
tabBtns.forEach(b => b.classList.remove('active'));
|
||||
tabContents.forEach(c => c.classList.remove('active'));
|
||||
|
||||
btn.classList.add('active');
|
||||
const targetId = btn.getAttribute('data-bs-target');
|
||||
document.getElementById(targetId).classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
// 动态增加链接输入框
|
||||
document.getElementById('addLinkButton').addEventListener('click', function() {
|
||||
var container = document.getElementById('linkInputsContainer');
|
||||
var newIndex = container.children.length;
|
||||
var newInput = document.createElement('input');
|
||||
newInput.type = 'text';
|
||||
newInput.className = '';
|
||||
newInput.id = `linkInput${newIndex}`;
|
||||
newInput.name = `linkInput${newIndex}`;
|
||||
newInput.placeholder = '请输入链接';
|
||||
newInput.style.cssText = 'flex: 1; padding: 10px 14px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px; outline: none;';
|
||||
|
||||
var removeButton = document.createElement('button');
|
||||
removeButton.type = 'button';
|
||||
removeButton.className = 'btn btn-secondary';
|
||||
removeButton.textContent = '×';
|
||||
removeButton.dataset.index = newIndex.toString();
|
||||
removeButton.addEventListener('click', function() {
|
||||
var group = this.closest('.input-group');
|
||||
container.removeChild(group);
|
||||
});
|
||||
|
||||
var inputGroup = document.createElement('div');
|
||||
inputGroup.className = 'input-group';
|
||||
inputGroup.appendChild(newInput);
|
||||
inputGroup.appendChild(removeButton);
|
||||
|
||||
container.appendChild(inputGroup);
|
||||
});
|
||||
|
||||
// 动态增加元数据行
|
||||
document.getElementById('addMetadataRow').addEventListener('click', function() {
|
||||
var container = document.getElementById('metadataTableBody');
|
||||
var newRow = document.createElement('tr');
|
||||
|
||||
newRow.innerHTML = `
|
||||
<td><input type="text" name="name[]" placeholder="歌曲名称" style="width: 100%; padding: 10px 14px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px; outline: none;"></td>
|
||||
<td><input type="text" name="artist[]" placeholder="艺术家" style="width: 100%; padding: 10px 14px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px; outline: none;"></td>
|
||||
<td><input type="text" name="album[]" placeholder="专辑" style="width: 100%; padding: 10px 14px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px; outline: none;"></td>
|
||||
`;
|
||||
|
||||
container.appendChild(newRow);
|
||||
});
|
||||
|
||||
document.getElementById('submitBtn').addEventListener('click', async function() {
|
||||
// 获取表单值
|
||||
let text = document.getElementById('textInput').value;
|
||||
let links = [];
|
||||
let local = [];
|
||||
let playlistName = '';
|
||||
|
||||
// 获取所有链接输入框的值
|
||||
let linkInputs = document.querySelectorAll('#linkInputsContainer .input-group input[type="text"]');
|
||||
linkInputs.forEach(function(input) {
|
||||
if (input.value.trim() !== '') {
|
||||
links.push(input.value);
|
||||
}
|
||||
});
|
||||
|
||||
// 获取元数据
|
||||
let metadataRows = document.querySelectorAll('#metadataTableBody tr');
|
||||
metadataRows.forEach(function(row) {
|
||||
let name = row.querySelector('input[name="name[]"]').value;
|
||||
let artist = row.querySelector('input[name="artist[]"]').value;
|
||||
let album = row.querySelector('input[name="album[]"]').value;
|
||||
if (name && artist && album) {
|
||||
local.push({ name, artist, album });
|
||||
}
|
||||
});
|
||||
|
||||
// 检查是否有且只有一个输入字段被填写
|
||||
let filledCount = (text ? 1 : 0) + (links.length > 0 ? 1 : 0) + (local.length > 0 ? 1 : 0);
|
||||
if (filledCount !== 1) {
|
||||
alert("请确保仅填写了一个输入字段!");
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取歌单名
|
||||
if (document.getElementById('importStarCheckbox').checked) {
|
||||
playlistName = '我喜欢的音乐';
|
||||
} else {
|
||||
playlistName = document.getElementById('playlistNameInput').value ||
|
||||
document.getElementById('playlistNameLinkInput').value ||
|
||||
'导入音乐 ' + new Date().toLocaleString();
|
||||
}
|
||||
|
||||
// 创建请求参数
|
||||
let data = {};
|
||||
if (text) {
|
||||
data.text = text;
|
||||
data.playlistName = playlistName;
|
||||
} else if (links.length > 0) {
|
||||
data.link = JSON.stringify(links);
|
||||
data.playlistName = playlistName;
|
||||
} else if (local.length > 0) {
|
||||
data.local = JSON.stringify(local);
|
||||
}
|
||||
|
||||
// 添加额外参数
|
||||
if (document.getElementById('importStarCheckbox').checked) {
|
||||
data.importStarPlaylist = true;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await axios({
|
||||
url: `/playlist/import/name/task/create?timestamp=${Date.now()}`,
|
||||
method: 'post',
|
||||
data: data,
|
||||
var removeButton = document.createElement('button');
|
||||
removeButton.type = 'button';
|
||||
removeButton.className = 'btn btn-secondary removeLinkButton';
|
||||
removeButton.textContent = '×';
|
||||
removeButton.dataset.index = newIndex.toString();
|
||||
removeButton.addEventListener('click', function() {
|
||||
var group = this.closest('.input-group');
|
||||
container.removeChild(group);
|
||||
});
|
||||
|
||||
let taskId = res.data?.data?.taskId
|
||||
if (taskId) {
|
||||
alert(`任务创建成功!正在导入,请稍等;任务id:${taskId}`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
alert('导入失败,请检查您的输入或稍后再试。');
|
||||
}
|
||||
});
|
||||
var inputGroup = document.createElement('div');
|
||||
inputGroup.className = 'input-group mb-3';
|
||||
inputGroup.appendChild(newInput);
|
||||
inputGroup.appendChild(removeButton);
|
||||
|
||||
// 监听复选框状态变化
|
||||
document.getElementById('importStarCheckbox').addEventListener('change', function() {
|
||||
let isChecked = this.checked;
|
||||
let playlistNameInputs = document.querySelectorAll('[name="playlistName"]');
|
||||
playlistNameInputs.forEach(function(input) {
|
||||
input.disabled = isChecked;
|
||||
container.appendChild(inputGroup);
|
||||
});
|
||||
});
|
||||
|
||||
// 初始化时设置歌单名输入框的状态
|
||||
document.getElementById('importStarCheckbox').dispatchEvent(new Event('change'));
|
||||
</script>
|
||||
// 动态增加元数据行
|
||||
document.getElementById('addMetadataRow').addEventListener('click', function() {
|
||||
var container = document.getElementById('metadataTableBody');
|
||||
var newRow = document.createElement('tr');
|
||||
|
||||
var nameInput = document.createElement('input');
|
||||
nameInput.type = 'text';
|
||||
nameInput.className = 'form-control';
|
||||
nameInput.name = 'name[]';
|
||||
nameInput.placeholder = '歌曲名称';
|
||||
|
||||
var artistInput = document.createElement('input');
|
||||
artistInput.type = 'text';
|
||||
artistInput.className = 'form-control';
|
||||
artistInput.name = 'artist[]';
|
||||
artistInput.placeholder = '艺术家';
|
||||
|
||||
var albumInput = document.createElement('input');
|
||||
albumInput.type = 'text';
|
||||
albumInput.className = 'form-control';
|
||||
albumInput.name = 'album[]';
|
||||
albumInput.placeholder = '专辑';
|
||||
|
||||
newRow.innerHTML = `
|
||||
<td>${nameInput.outerHTML}</td>
|
||||
<td>${artistInput.outerHTML}</td>
|
||||
<td>${albumInput.outerHTML}</td>
|
||||
`;
|
||||
|
||||
container.appendChild(newRow);
|
||||
});
|
||||
|
||||
document.getElementById('importForm').addEventListener('submit', async function(event) {
|
||||
// 阻止默认行为
|
||||
event.preventDefault();
|
||||
|
||||
// 获取表单值
|
||||
let text = document.getElementById('textInput').value;
|
||||
let links = [];
|
||||
let local = [];
|
||||
let playlistName = '';
|
||||
|
||||
// 获取所有链接输入框的值
|
||||
let linkInputs = document.querySelectorAll('#linkInputsContainer .input-group .form-control');
|
||||
linkInputs.forEach(function(input) {
|
||||
if (input.value.trim() !== '') {
|
||||
links.push(input.value);
|
||||
}
|
||||
});
|
||||
|
||||
// 获取元数据
|
||||
let metadataRows = document.querySelectorAll('#metadataTableBody tr');
|
||||
metadataRows.forEach(function(row) {
|
||||
let name = row.querySelector('input[name="name[]"]').value;
|
||||
let artist = row.querySelector('input[name="artist[]"]').value;
|
||||
let album = row.querySelector('input[name="album[]"]').value;
|
||||
if (name && artist && album) {
|
||||
local.push({ name, artist, album });
|
||||
}
|
||||
});
|
||||
|
||||
// 检查是否有且只有一个输入字段被填写
|
||||
let filledCount = (text ? 1 : 0) + (links.length > 0 ? 1 : 0) + (local.length > 0 ? 1 : 0);
|
||||
if (filledCount !== 1) {
|
||||
alert("请确保仅填写了一个输入字段!");
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取歌单名
|
||||
if (document.getElementById('importStarCheckbox').checked) {
|
||||
playlistName = '我喜欢的音乐';
|
||||
} else {
|
||||
playlistName = document.getElementById('playlistNameInput').value ||
|
||||
document.getElementById('playlistNameLinkInput').value ||
|
||||
'导入音乐 ' + new Date().toLocaleString();
|
||||
}
|
||||
|
||||
// 创建请求参数
|
||||
let data = {};
|
||||
if (text) {
|
||||
data.text = text;
|
||||
data.playlistName = playlistName;
|
||||
} else if (links.length > 0) {
|
||||
data.link = JSON.stringify(links);
|
||||
data.playlistName = playlistName;
|
||||
} else if (local.length > 0) {
|
||||
data.local = JSON.stringify(local);
|
||||
}
|
||||
|
||||
// 添加额外参数
|
||||
if (document.getElementById('importStarCheckbox').checked) {
|
||||
data.importStarPlaylist = true;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await axios({
|
||||
url: `/playlist/import/name/task/create?timestamp=${Date.now()}`,
|
||||
method: 'post',
|
||||
data: data,
|
||||
});
|
||||
|
||||
let taskId = res.data?.data?.taskId
|
||||
if (taskId) {
|
||||
alert(`任务创建成功! 正在导入, 请稍等; 任务id:${taskId}`)
|
||||
// const res2 = await axios({
|
||||
// url: `/playlist/import/task/status?timestamp=${Date.now()}`,
|
||||
// method: 'post',
|
||||
// data: {
|
||||
// id: taskId
|
||||
// },
|
||||
// });
|
||||
// alert(JSON.stringify(res2.data, null, 2));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
alert('导入失败,请检查您的输入或稍后再试。');
|
||||
}
|
||||
});
|
||||
|
||||
// 监听复选框状态变化
|
||||
document.getElementById('importStarCheckbox').addEventListener('change', function() {
|
||||
let isChecked = this.checked;
|
||||
let playlistNameInputs = document.querySelectorAll('[name="playlistName"]');
|
||||
playlistNameInputs.forEach(function(input) {
|
||||
input.disabled = isChecked;
|
||||
});
|
||||
});
|
||||
|
||||
// 初始化时设置歌单名输入框的状态
|
||||
document.getElementById('importStarCheckbox').dispatchEvent(new Event('change'));
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -5,164 +5,45 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>二维码登录</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 48px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
max-width: 450px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.qr-wrapper {
|
||||
display: inline-block;
|
||||
padding: 16px;
|
||||
background: #f9f9f9;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
#qrImg {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.info {
|
||||
text-align: left;
|
||||
padding: 16px;
|
||||
background: #f9f9f9;
|
||||
border-radius: 6px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.status {
|
||||
margin-top: 16px;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status.waiting {
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.status.success {
|
||||
background: #d1fae5;
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
.status.error {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.hint {
|
||||
margin-top: 16px;
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>二维码登录</h1>
|
||||
<p class="subtitle">使用网易云音乐App扫描二维码登录</p>
|
||||
|
||||
<div class="qr-wrapper">
|
||||
<img id="qrImg" src="" alt="二维码加载中..." />
|
||||
</div>
|
||||
|
||||
<div id="status" class="status waiting">等待扫描...</div>
|
||||
|
||||
<div id="info" class="info"></div>
|
||||
|
||||
<p class="hint">请打开网易云音乐App,扫描上方二维码完成登录</p>
|
||||
</div>
|
||||
|
||||
<script src="https://fastly.jsdelivr.net/npm/[email protected]/dist/axios.min.js"></script>
|
||||
<img id="qrImg" />
|
||||
<div id="info" class="info"></div>
|
||||
<script src="https://fastly.jsdelivr.net/npm/[email protected]/dist/axios.min.js
|
||||
"></script>
|
||||
<script>
|
||||
|
||||
async function login() {
|
||||
let timer
|
||||
const statusDiv = document.getElementById('status')
|
||||
let timestamp = Date.now()
|
||||
const cookie = localStorage.getItem('cookie')
|
||||
|
||||
updateStatus('加载二维码...', 'waiting')
|
||||
getLoginStatus(cookie)
|
||||
const res = await axios({
|
||||
url: `/login/qr/key?timestamp=${Date.now()}`,
|
||||
})
|
||||
const key = res.data.data.unikey
|
||||
const res2 = await axios({
|
||||
url: `/login/qr/create?key=${key}&platform=web&qrimg=true×tamp=${Date.now()}`,
|
||||
})
|
||||
document.querySelector('#qrImg').src = res2.data.data.qrimg
|
||||
|
||||
try {
|
||||
const res = await axios({
|
||||
url: `/login/qr/key?timestamp=${Date.now()}`,
|
||||
})
|
||||
const key = res.data.data.unikey
|
||||
|
||||
const res2 = await axios({
|
||||
url: `/login/qr/create?key=${key}&platform=web&qrimg=true×tamp=${Date.now()}`,
|
||||
})
|
||||
document.querySelector('#qrImg').src = res2.data.data.qrimg
|
||||
updateStatus('请扫描二维码', 'waiting')
|
||||
|
||||
timer = setInterval(async () => {
|
||||
const statusRes = await checkStatus(key)
|
||||
if (statusRes.code === 800) {
|
||||
updateStatus('二维码已过期,请刷新页面', 'error')
|
||||
clearInterval(timer)
|
||||
} else if (statusRes.code === 801) {
|
||||
updateStatus('二维码已扫描,请在手机上确认', 'waiting')
|
||||
} else if (statusRes.code === 802) {
|
||||
updateStatus('登录成功,正在保存信息...', 'waiting')
|
||||
} else if (statusRes.code === 803) {
|
||||
clearInterval(timer)
|
||||
updateStatus('授权登录成功!', 'success')
|
||||
await getLoginStatus(statusRes.cookie)
|
||||
localStorage.setItem('cookie', statusRes.cookie)
|
||||
}
|
||||
}, 3000)
|
||||
} catch (error) {
|
||||
console.error('登录失败:', error)
|
||||
updateStatus('二维码加载失败,请刷新页面重试', 'error')
|
||||
}
|
||||
timer = setInterval(async () => {
|
||||
const statusRes = await checkStatus(key)
|
||||
if (statusRes.code === 800) {
|
||||
alert('二维码已过期,请重新获取')
|
||||
clearInterval(timer)
|
||||
}
|
||||
if (statusRes.code === 803) {
|
||||
// 这一步会返回cookie
|
||||
clearInterval(timer)
|
||||
alert('授权登录成功')
|
||||
await getLoginStatus(statusRes.cookie)
|
||||
localStorage.setItem('cookie', statusRes.cookie)
|
||||
}
|
||||
}, 3000)
|
||||
}
|
||||
login()
|
||||
|
||||
async function checkStatus(key) {
|
||||
const res = await axios({
|
||||
@@ -170,30 +51,22 @@
|
||||
})
|
||||
return res.data
|
||||
}
|
||||
|
||||
async function getLoginStatus(cookie = '') {
|
||||
try {
|
||||
const res = await axios({
|
||||
url: `/login/status?timestamp=${Date.now()}`,
|
||||
method: 'post',
|
||||
data: {
|
||||
cookie,
|
||||
},
|
||||
})
|
||||
document.querySelector('#info').textContent = JSON.stringify(res.data, null, 2)
|
||||
} catch (error) {
|
||||
console.error('获取登录状态失败:', error)
|
||||
}
|
||||
const res = await axios({
|
||||
url: `/login/status?timestamp=${Date.now()}`,
|
||||
method: 'post',
|
||||
data: {
|
||||
cookie,
|
||||
},
|
||||
})
|
||||
document.querySelector('#info').innerText = JSON.stringify(res.data, null, 2)
|
||||
}
|
||||
|
||||
function updateStatus(message, type) {
|
||||
const statusDiv = document.getElementById('status')
|
||||
statusDiv.textContent = message
|
||||
statusDiv.className = 'status ' + type
|
||||
}
|
||||
|
||||
login()
|
||||
</script>
|
||||
<style>
|
||||
.info {
|
||||
white-space: pre;
|
||||
}
|
||||
</style>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
@@ -5,195 +5,67 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>二维码登录</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 48px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
max-width: 450px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.qr-wrapper {
|
||||
display: inline-block;
|
||||
padding: 16px;
|
||||
background: #f9f9f9;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
#qrImg {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.info {
|
||||
text-align: left;
|
||||
padding: 16px;
|
||||
background: #f9f9f9;
|
||||
border-radius: 6px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.status {
|
||||
margin-top: 16px;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status.waiting {
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.status.success {
|
||||
background: #d1fae5;
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
.status.error {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.hint {
|
||||
margin-top: 16px;
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>二维码登录</h1>
|
||||
<p class="subtitle">使用网易云音乐App扫描二维码登录</p>
|
||||
|
||||
<div class="qr-wrapper">
|
||||
<img id="qrImg" src="" alt="二维码加载中..." />
|
||||
</div>
|
||||
|
||||
<div id="status" class="status waiting">等待扫描...</div>
|
||||
|
||||
<div id="info" class="info"></div>
|
||||
|
||||
<p class="hint">请打开网易云音乐App,扫描上方二维码完成登录</p>
|
||||
</div>
|
||||
|
||||
<script src="https://fastly.jsdelivr.net/npm/[email protected]/dist/axios.min.js"></script>
|
||||
<img id="qrImg" />
|
||||
<div id="info" class="info"></div>
|
||||
<script src="https://fastly.jsdelivr.net/npm/[email protected]/dist/axios.min.js
|
||||
"></script>
|
||||
<script>
|
||||
async function login() {
|
||||
let timer
|
||||
const statusDiv = document.getElementById('status')
|
||||
let timestamp = Date.now()
|
||||
const cookie = localStorage.getItem('cookie')
|
||||
|
||||
updateStatus('加载二维码...', 'waiting')
|
||||
getLoginStatus(cookie)
|
||||
const res = await axios({
|
||||
url: `/login/qr/key?timestamp=${Date.now()}`,
|
||||
})
|
||||
const key = res.data.data.unikey
|
||||
const res2 = await axios({
|
||||
url: `/login/qr/create?key=${key}&platform=web&qrimg=true×tamp=${Date.now()}`,
|
||||
})
|
||||
document.querySelector('#qrImg').src = res2.data.data.qrimg
|
||||
|
||||
try {
|
||||
const res = await axios({
|
||||
url: `/login/qr/key?timestamp=${Date.now()}`,
|
||||
})
|
||||
const key = res.data.data.unikey
|
||||
|
||||
const res2 = await axios({
|
||||
url: `/login/qr/create?key=${key}&platform=web&qrimg=true×tamp=${Date.now()}&ua=pc`,
|
||||
})
|
||||
document.querySelector('#qrImg').src = res2.data.data.qrimg
|
||||
updateStatus('请扫描二维码', 'waiting')
|
||||
|
||||
timer = setInterval(async () => {
|
||||
const statusRes = await checkStatus(key)
|
||||
if (statusRes.code === 800) {
|
||||
updateStatus('二维码已过期,请刷新页面', 'error')
|
||||
clearInterval(timer)
|
||||
} else if (statusRes.code === 801) {
|
||||
updateStatus('二维码已扫描,请在手机上确认', 'waiting')
|
||||
} else if (statusRes.code === 802) {
|
||||
updateStatus('登录成功,正在保存信息...', 'waiting')
|
||||
} else if (statusRes.code === 803) {
|
||||
clearInterval(timer)
|
||||
updateStatus('授权登录成功!', 'success')
|
||||
await getLoginStatus(statusRes.cookie)
|
||||
localStorage.setItem('cookie', statusRes.cookie)
|
||||
}
|
||||
}, 3000)
|
||||
} catch (error) {
|
||||
console.error('登录失败:', error)
|
||||
updateStatus('二维码加载失败,请刷新页面重试', 'error')
|
||||
}
|
||||
timer = setInterval(async () => {
|
||||
const statusRes = await checkStatus(key)
|
||||
if (statusRes.code === 800) {
|
||||
alert('二维码已过期,请重新获取')
|
||||
clearInterval(timer)
|
||||
}
|
||||
if (statusRes.code === 803) {
|
||||
// 这一步会返回cookie
|
||||
clearInterval(timer)
|
||||
alert('授权登录成功')
|
||||
await getLoginStatus(statusRes.cookie)
|
||||
localStorage.setItem('cookie', statusRes.cookie)
|
||||
}
|
||||
}, 3000)
|
||||
}
|
||||
login()
|
||||
|
||||
async function checkStatus(key) {
|
||||
const res = await axios({
|
||||
url: `/login/qr/check?key=${key}×tamp=${Date.now()}&ua=pc`,
|
||||
url: `/login/qr/check?key=${key}×tamp=${Date.now()}`,
|
||||
})
|
||||
return res.data
|
||||
}
|
||||
|
||||
async function getLoginStatus(cookie = '') {
|
||||
try {
|
||||
const res = await axios({
|
||||
url: `/login/status?timestamp=${Date.now()}&ua=pc`,
|
||||
method: 'post',
|
||||
data: {
|
||||
cookie,
|
||||
},
|
||||
})
|
||||
document.querySelector('#info').textContent = JSON.stringify(res.data, null, 2)
|
||||
} catch (error) {
|
||||
console.error('获取登录状态失败:', error)
|
||||
}
|
||||
const res = await axios({
|
||||
url: `/login/status?timestamp=${Date.now()}`,
|
||||
method: 'post',
|
||||
data: {
|
||||
cookie,
|
||||
},
|
||||
})
|
||||
document.querySelector('#info').innerText = JSON.stringify(res.data, null, 2)
|
||||
}
|
||||
|
||||
function updateStatus(message, type) {
|
||||
const statusDiv = document.getElementById('status')
|
||||
statusDiv.textContent = message
|
||||
statusDiv.className = 'status ' + type
|
||||
}
|
||||
|
||||
login()
|
||||
</script>
|
||||
<style>
|
||||
.info {
|
||||
white-space: pre;
|
||||
}
|
||||
</style>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
|
Before Width: | Height: | Size: 992 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 627 KiB |
|
Before Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 619 KiB |
|
After Width: | Height: | Size: 243 KiB |
|
After Width: | Height: | Size: 659 KiB |
@@ -1,153 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>音乐解灰测试</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 32px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #555;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
font-size: 15px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
border-color: #333;
|
||||
}
|
||||
|
||||
button {
|
||||
background: #333;
|
||||
color: white;
|
||||
padding: 14px 28px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
background: #999;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
#result {
|
||||
margin-top: 24px;
|
||||
padding: 16px;
|
||||
background: #f9f9f9;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #eee;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
.hint {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-top: 4px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>音乐解灰测试</h1>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="songId">音乐 ID</label>
|
||||
<input type="number" id="songId" placeholder="请输入音乐ID" />
|
||||
<div class="hint">例如: 1372188635</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="sources">音源列表(可选)</label>
|
||||
<input type="text" id="sources" placeholder="请输入音源" />
|
||||
<div class="hint">例如: kuwo, kugou, migu</div>
|
||||
</div>
|
||||
|
||||
<button id="testBtn" onclick="testSong()">开始测试</button>
|
||||
|
||||
<div id="result"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function testSong() {
|
||||
const songId = document.getElementById('songId').value;
|
||||
const sources = document.getElementById('sources').value;
|
||||
const testBtn = document.getElementById('testBtn');
|
||||
const resultDiv = document.getElementById('result');
|
||||
|
||||
if (!songId) {
|
||||
alert('请输入音乐ID');
|
||||
return;
|
||||
}
|
||||
|
||||
testBtn.disabled = true;
|
||||
testBtn.textContent = '测试中...';
|
||||
resultDiv.textContent = '正在请求...';
|
||||
|
||||
try {
|
||||
const response = await fetch(`/song/url/match?id=${songId}&source=${sources}`);
|
||||
const data = await response.json();
|
||||
resultDiv.textContent = JSON.stringify(data, null, 2);
|
||||
} catch (error) {
|
||||
resultDiv.textContent = `请求失败: ${error.message}`;
|
||||
} finally {
|
||||
testBtn.disabled = false;
|
||||
testBtn.textContent = '开始测试';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,326 +1,125 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>播客上传声音</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
padding: 20px;
|
||||
}
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>播客上传声音</title>
|
||||
</head>
|
||||
|
||||
.container {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 32px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
<body>
|
||||
<div>
|
||||
<a href="/qrlogin-nocookie.html">
|
||||
如果没登录,请先登录
|
||||
</a>
|
||||
</div>
|
||||
<div id="app">
|
||||
<ul>
|
||||
<li v-for="(item,index) in voicelist" @click="currentVoiceIndex=index"
|
||||
:class="{active:currentVoiceIndex===index}">
|
||||
<img :src="item.coverUrl" style="width:50px;width:50px;" />
|
||||
<ul>
|
||||
<li v-for="(item2,index) in item.voiceListData">
|
||||
{{item2.voiceName}}
|
||||
</li>
|
||||
</ul>
|
||||
{{item.voiceListName}}
|
||||
</li>
|
||||
</ul>
|
||||
<input v-model="songName" placeholder="请输入声音名称" />
|
||||
<input v-model="description" placeholder="请输入介绍" />
|
||||
<input type="file" name="songFile" />
|
||||
<button @click="submit">上传</button>
|
||||
</div>
|
||||
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.login-link {
|
||||
display: block;
|
||||
margin-bottom: 24px;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.login-link:hover {
|
||||
color: #333;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.voice-list {
|
||||
flex: 1;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.voice-item {
|
||||
padding: 12px 16px;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.voice-item:hover {
|
||||
border-color: #333;
|
||||
background: #f9f9f9;
|
||||
}
|
||||
|
||||
.voice-item.active {
|
||||
border-color: #333;
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.voice-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.voice-cover {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 4px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.voice-name {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.voice-tracks {
|
||||
margin-top: 8px;
|
||||
padding-left: 62px;
|
||||
}
|
||||
|
||||
.voice-track {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.upload-section {
|
||||
flex: 1;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #555;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
input[type="text"], input[type="file"] {
|
||||
width: 100%;
|
||||
padding: 10px 14px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
input[type="text"]:focus {
|
||||
border-color: #333;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
background: #333;
|
||||
color: white;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>播客上传声音</h1>
|
||||
<a href="/qrlogin-nocookie.html" class="login-link">还没登录?点击登录</a>
|
||||
|
||||
<div class="content">
|
||||
<div class="voice-list">
|
||||
<h3 style="font-size: 16px; font-weight: 600; color: #333; margin-bottom: 16px;">选择播客列表</h3>
|
||||
<div v-if="loading" class="loading">加载中...</div>
|
||||
<div v-else-if="voicelist.length > 0">
|
||||
<div
|
||||
v-for="(item, index) in voicelist"
|
||||
:key="index"
|
||||
@click="currentVoiceIndex = index"
|
||||
:class="{ active: currentVoiceIndex === index }"
|
||||
class="voice-item"
|
||||
>
|
||||
<div class="voice-header">
|
||||
<img :src="item.coverUrl" class="voice-cover" alt="cover" />
|
||||
<span class="voice-name">{{ item.voiceListName }}</span>
|
||||
</div>
|
||||
<div class="voice-tracks" v-if="item.voiceListData">
|
||||
<div
|
||||
v-for="(item2, index2) in item.voiceListData"
|
||||
:key="index2"
|
||||
class="voice-track"
|
||||
>
|
||||
{{ item2.voiceName }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="empty-state">暂无播客列表</div>
|
||||
</div>
|
||||
|
||||
<div class="upload-section">
|
||||
<h3 style="font-size: 16px; font-weight: 600; color: #333; margin-bottom: 16px;">上传声音</h3>
|
||||
<div class="form-group">
|
||||
<label for="songName">声音名称</label>
|
||||
<input id="songName" v-model="songName" placeholder="请输入声音名称" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="description">介绍</label>
|
||||
<input id="description" v-model="description" placeholder="请输入介绍" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>选择文件</label>
|
||||
<input type="file" name="songFile" accept="audio/*" />
|
||||
</div>
|
||||
|
||||
<button class="btn" @click="submit">上传</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://fastly.jsdelivr.net/npm/[email protected]/dist/axios.min.js"></script>
|
||||
<script src="https://fastly.jsdelivr.net/npm/vue@3"></script>
|
||||
<script>
|
||||
Vue.createApp({
|
||||
data() {
|
||||
return {
|
||||
songName: '',
|
||||
description: '',
|
||||
voicelist: [],
|
||||
cookieToken: '',
|
||||
currentVoiceIndex: 0,
|
||||
loading: false,
|
||||
}
|
||||
<script src="https://fastly.jsdelivr.net/npm/[email protected]/dist/axios.min.js"></script>
|
||||
<script src="https://fastly.jsdelivr.net/npm/vue"></script>
|
||||
<script>
|
||||
Vue.createApp({
|
||||
data() {
|
||||
return {
|
||||
songName: '',
|
||||
description: '',
|
||||
voicelist: [],
|
||||
cookieToken: '',
|
||||
currentVoiceIndex: 0,
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.getData()
|
||||
},
|
||||
computed: {
|
||||
currentVoice() {
|
||||
// {
|
||||
// voiceListId: '',
|
||||
// coverImgId: '',
|
||||
// categoryId: '',
|
||||
// secondCategoryId: '',
|
||||
// }
|
||||
return this.voicelist[this.currentVoiceIndex]
|
||||
},
|
||||
created() {
|
||||
this.getData()
|
||||
},
|
||||
methods: {
|
||||
submit() {
|
||||
console.log('submit')
|
||||
const file = document.querySelector('input[type=file]').files[0]
|
||||
this.upload(file)
|
||||
},
|
||||
computed: {
|
||||
currentVoice() {
|
||||
return this.voicelist[this.currentVoiceIndex]
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
submit() {
|
||||
console.info('submit')
|
||||
const file = document.querySelector('input[type=file]').files[0]
|
||||
if (!file) {
|
||||
alert('请选择文件')
|
||||
return
|
||||
}
|
||||
this.upload(file)
|
||||
},
|
||||
|
||||
async getData() {
|
||||
this.loading = true
|
||||
try {
|
||||
const res = await axios({
|
||||
url: `/voicelist/search?cookie=${localStorage.getItem('cookie')}`,
|
||||
})
|
||||
async getData() {
|
||||
const res = await axios({
|
||||
url: `/voicelist/search?cookie=${localStorage.getItem('cookie')}`,
|
||||
})
|
||||
|
||||
console.info(res.data.data)
|
||||
this.voicelist = res.data.data.list || []
|
||||
this.voicelist.forEach(async (i) => {
|
||||
try {
|
||||
const res2 = await axios({
|
||||
url: `/voicelist/list?voiceListId=${i.voiceListId}&limit=5`,
|
||||
})
|
||||
i.voiceListData = res2.data.data.list || []
|
||||
console.info(res2)
|
||||
} catch (err) {
|
||||
console.error('获取播客详情失败:', err)
|
||||
}
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('获取播客列表失败:', err)
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
upload(file) {
|
||||
if (!this.currentVoice) {
|
||||
alert('请先选择播客列表')
|
||||
return
|
||||
}
|
||||
|
||||
var formData = new FormData()
|
||||
formData.append('songFile', file)
|
||||
|
||||
axios({
|
||||
method: 'post',
|
||||
url: `/voice/upload?time=${Date.now()}&cookie=${localStorage.getItem(
|
||||
'cookie',
|
||||
)}&songName=${this.songName}&voiceListId=${
|
||||
this.currentVoice.voiceListId
|
||||
}&categoryId=${this.currentVoice.categoryId}&coverImgId=${
|
||||
this.currentVoice.coverImgId
|
||||
}&secondCategoryId=${
|
||||
this.currentVoice.secondCategoryId
|
||||
}&description=${this.description}&privacy=1`,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
data: formData,
|
||||
console.log(res.data.data)
|
||||
this.voicelist = res.data.data.list
|
||||
this.voicelist.map(async i => {
|
||||
const res2 = await axios({
|
||||
url: `/voicelist/list?voiceListId=${i.voiceListId}&limit=5`,
|
||||
})
|
||||
.then((res) => {
|
||||
alert(`${file.name} 上传成功`)
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('上传失败:', err)
|
||||
alert('上传失败,请重试')
|
||||
})
|
||||
},
|
||||
i.voiceListData = res2.data.data.list
|
||||
console.log(res2);
|
||||
})
|
||||
},
|
||||
}).mount('body')
|
||||
</script>
|
||||
</body>
|
||||
upload(file) {
|
||||
var formData = new FormData()
|
||||
formData.append('songFile', file)
|
||||
axios({
|
||||
method: 'post',
|
||||
url: `/voice/upload?time=${Date.now()}&cookie=${localStorage.getItem('cookie')
|
||||
}&songName=${this.songName}&voiceListId=${this.currentVoice.voiceListId
|
||||
}&categoryId=${this.currentVoice.categoryId}&coverImgId=${this.currentVoice.coverImgId
|
||||
}&secondCategoryId=${this.currentVoice.secondCategoryId}&description=${this.description
|
||||
}&privacy=1`,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
data: formData,
|
||||
})
|
||||
.then((res) => {
|
||||
alert(`${file.name} 上传成功`)
|
||||
if (currentIndx >= fileLength) {
|
||||
console.log('上传完毕')
|
||||
}
|
||||
})
|
||||
.catch(async (err) => {
|
||||
console.log(err)
|
||||
})
|
||||
},
|
||||
},
|
||||
}).mount('#app')
|
||||
</script>
|
||||
|
||||
<style>
|
||||
ul li {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
ul li.active {
|
||||
color: red;
|
||||
}
|
||||
</style>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": [
|
||||
"config:base",
|
||||
"helpers:pinGitHubActionDigests"
|
||||
]
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
require('dotenv').config()
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const express = require('express')
|
||||
@@ -9,8 +8,7 @@ const cache = require('./util/apicache').middleware
|
||||
const { cookieToJson } = require('./util/index')
|
||||
const fileUpload = require('express-fileupload')
|
||||
const decode = require('safe-decode-uri-component')
|
||||
const logger = require('./util/logger.js')
|
||||
|
||||
require("dotenv").config();
|
||||
/**
|
||||
* The version check result.
|
||||
* @readonly
|
||||
@@ -99,7 +97,7 @@ async function getModulesDefinitions(
|
||||
*/
|
||||
async function checkVersion() {
|
||||
return new Promise((resolve) => {
|
||||
exec('npm info NeteaseCloudMusicApiEnhanced version', (err, stdout) => {
|
||||
exec('npm info NeteaseCloudMusicApi version', (err, stdout) => {
|
||||
if (!err) {
|
||||
let version = stdout.trim()
|
||||
|
||||
@@ -127,68 +125,15 @@ async function checkVersion() {
|
||||
})
|
||||
}
|
||||
|
||||
function parseCorsAllowOrigins(corsAllowOrigin) {
|
||||
if (!corsAllowOrigin) {
|
||||
return null
|
||||
}
|
||||
|
||||
const origins = corsAllowOrigin
|
||||
.split(',')
|
||||
.map((origin) => origin.trim())
|
||||
.filter(Boolean)
|
||||
|
||||
return origins.length > 0 ? origins : null
|
||||
}
|
||||
|
||||
function getCorsAllowOrigin(allowOrigins, requestOrigin) {
|
||||
if (!allowOrigins) {
|
||||
return requestOrigin || '*'
|
||||
}
|
||||
|
||||
if (allowOrigins.includes('*')) {
|
||||
return '*'
|
||||
}
|
||||
|
||||
if (requestOrigin && allowOrigins.includes(requestOrigin)) {
|
||||
return requestOrigin
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function createConsoleSpinner(message = '启动中') {
|
||||
if (!process.stdout.isTTY) {
|
||||
return {
|
||||
stop() {},
|
||||
}
|
||||
}
|
||||
|
||||
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
|
||||
let index = 0
|
||||
process.stdout.write(`${frames[index]} ${message}...`)
|
||||
const timer = setInterval(() => {
|
||||
index = (index + 1) % frames.length
|
||||
process.stdout.write(`\r${frames[index]} ${message}...`)
|
||||
}, 80)
|
||||
|
||||
return {
|
||||
stop() {
|
||||
clearInterval(timer)
|
||||
process.stdout.write(`\r✔ ${message} 完成。\n`)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct the server of NCM API.
|
||||
*
|
||||
* @param {ModuleDefinition[]} [moduleDefs] Customized module definitions [advanced]
|
||||
* @returns {Promise<import("express").Express>} The server instance.
|
||||
*/
|
||||
async function constructServer(moduleDefs) {
|
||||
async function consturctServer(moduleDefs) {
|
||||
const app = express()
|
||||
const { CORS_ALLOW_ORIGIN } = process.env
|
||||
const allowOrigins = parseCorsAllowOrigins(CORS_ALLOW_ORIGIN)
|
||||
app.set('trust proxy', true)
|
||||
|
||||
/**
|
||||
@@ -200,17 +145,9 @@ async function constructServer(moduleDefs) {
|
||||
*/
|
||||
app.use((req, res, next) => {
|
||||
if (req.path !== '/' && !req.path.includes('.')) {
|
||||
const corsAllowOrigin = getCorsAllowOrigin(
|
||||
allowOrigins,
|
||||
req.headers.origin,
|
||||
)
|
||||
const shouldSetVaryHeader = corsAllowOrigin && corsAllowOrigin !== '*'
|
||||
res.set({
|
||||
'Access-Control-Allow-Credentials': true,
|
||||
...(corsAllowOrigin
|
||||
? { 'Access-Control-Allow-Origin': corsAllowOrigin }
|
||||
: {}),
|
||||
...(shouldSetVaryHeader ? { Vary: 'Origin' } : {}),
|
||||
'Access-Control-Allow-Origin': CORS_ALLOW_ORIGIN || req.headers.origin || '*',
|
||||
'Access-Control-Allow-Headers': 'X-Requested-With,Content-Type',
|
||||
'Access-Control-Allow-Methods': 'PUT,POST,GET,DELETE,OPTIONS',
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
@@ -238,25 +175,10 @@ async function constructServer(moduleDefs) {
|
||||
/**
|
||||
* Body Parser and File Upload
|
||||
*/
|
||||
const MAX_UPLOAD_SIZE_MB = 500
|
||||
const MAX_UPLOAD_SIZE_BYTES = MAX_UPLOAD_SIZE_MB * 1024 * 1024
|
||||
app.use(express.json({ limit: '50mb' }))
|
||||
app.use(express.urlencoded({ extended: false, limit: '50mb' }))
|
||||
|
||||
app.use(express.json({ limit: `${MAX_UPLOAD_SIZE_MB}mb` }))
|
||||
app.use(
|
||||
express.urlencoded({ extended: false, limit: `${MAX_UPLOAD_SIZE_MB}mb` }),
|
||||
)
|
||||
|
||||
app.use(
|
||||
fileUpload({
|
||||
limits: {
|
||||
fileSize: MAX_UPLOAD_SIZE_BYTES,
|
||||
},
|
||||
useTempFiles: true,
|
||||
tempFileDir: require('os').tmpdir(),
|
||||
abortOnLimit: true,
|
||||
parseNested: true,
|
||||
}),
|
||||
)
|
||||
app.use(fileUpload())
|
||||
|
||||
/**
|
||||
* Cache
|
||||
@@ -281,11 +203,9 @@ async function constructServer(moduleDefs) {
|
||||
|
||||
for (const moduleDef of moduleDefinitions) {
|
||||
// Register the route.
|
||||
app.all(moduleDef.route, async (req, res) => {
|
||||
app.use(moduleDef.route, async (req, res) => {
|
||||
;[req.query, req.body].forEach((item) => {
|
||||
// item may be undefined (some environments / middlewares).
|
||||
// Guard access to avoid "Cannot read properties of undefined (reading 'cookie')".
|
||||
if (item && typeof item.cookie === 'string') {
|
||||
if (typeof item.cookie === 'string') {
|
||||
item.cookie = cookieToJson(decode(item.cookie))
|
||||
}
|
||||
})
|
||||
@@ -302,56 +222,36 @@ async function constructServer(moduleDefs) {
|
||||
const moduleResponse = await moduleDef.module(query, (...params) => {
|
||||
// 参数注入客户端IP
|
||||
const obj = [...params]
|
||||
const options = obj[2] || {}
|
||||
if (!options.randomCNIP) {
|
||||
let ip = req.ip
|
||||
let ip = req.ip
|
||||
|
||||
if (ip.substring(0, 7) == '::ffff:') {
|
||||
ip = ip.substring(7)
|
||||
}
|
||||
if (ip == '::1') {
|
||||
ip = global.cnIp
|
||||
}
|
||||
// logger.info('Requested from ip:', ip)
|
||||
obj[2] = {
|
||||
...options,
|
||||
ip,
|
||||
}
|
||||
if (ip.substring(0, 7) == '::ffff:') {
|
||||
ip = ip.substring(7)
|
||||
}
|
||||
if (ip == '::1') {
|
||||
ip = global.cnIp
|
||||
}
|
||||
// console.log(ip)
|
||||
obj[3] = {
|
||||
...obj[3],
|
||||
ip,
|
||||
}
|
||||
|
||||
return request(...obj)
|
||||
})
|
||||
logger.info(`Request Success: ${decode(req.originalUrl)}`)
|
||||
console.log('[OK]', decode(req.originalUrl))
|
||||
|
||||
// 夹带私货部分:如果开启了通用解锁,并且是获取歌曲URL的接口,则尝试解锁(如果需要的话)ヾ(≧▽≦*)o
|
||||
if (
|
||||
req.baseUrl === '/song/url/v1' &&
|
||||
process.env.ENABLE_GENERAL_UNBLOCK === 'true'
|
||||
) {
|
||||
const song = moduleResponse.body.data[0]
|
||||
if (
|
||||
song.freeTrialInfo !== null ||
|
||||
!song.url ||
|
||||
[1, 4].includes(song.fee)
|
||||
) {
|
||||
const {
|
||||
matchID,
|
||||
} = require('@neteasecloudmusicapienhanced/unblockmusic-utils')
|
||||
logger.info('Starting unblock(uses general unblock):', req.query.id)
|
||||
const result = await matchID(req.query.id)
|
||||
song.url = result.data.url
|
||||
song.freeTrialInfo = null
|
||||
logger.info('Unblock success! url:', song.url)
|
||||
}
|
||||
if (song.url && song.url.includes('kuwo')) {
|
||||
const proxy = process.env.PROXY_URL
|
||||
const useProxy = process.env.ENABLE_PROXY || 'false'
|
||||
if (useProxy === 'true' && proxy) {
|
||||
song.proxyUrl = proxy + song.url
|
||||
if (req.baseUrl === '/song/url/v1' || req.baseUrl === '/song/url') {
|
||||
const song = moduleResponse['body']['data'][0]
|
||||
if (song.freeTrialInfo !== null || !song.url || [1, 4].includes(song.fee)) {
|
||||
const match = require('@unblockneteasemusic/server')
|
||||
const source = ['pyncmd', 'kuwo']
|
||||
const { url } = await match(req.query.id, source)
|
||||
song.url = url
|
||||
song.freeTrialInfo = 'unblock'
|
||||
console.log("解灰成功!")// 对于Splayer来说,去除开通会员提示
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const cookies = moduleResponse.cookie
|
||||
if (!query.noCookie) {
|
||||
if (Array.isArray(cookies) && cookies.length > 0) {
|
||||
@@ -368,14 +268,9 @@ async function constructServer(moduleDefs) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (moduleResponse.redirectUrl) {
|
||||
res.redirect(moduleResponse.status || 302, moduleResponse.redirectUrl)
|
||||
return
|
||||
}
|
||||
|
||||
res.status(moduleResponse.status).send(moduleResponse.body)
|
||||
} catch (/** @type {*} */ moduleResponse) {
|
||||
logger.error(`${decode(req.originalUrl)}`, {
|
||||
console.log('[ERR]', decode(req.originalUrl), {
|
||||
status: moduleResponse.status,
|
||||
body: moduleResponse.body,
|
||||
})
|
||||
@@ -410,39 +305,26 @@ async function serveNcmApi(options) {
|
||||
const port = Number(options.port || process.env.PORT || '3000')
|
||||
const host = options.host || process.env.HOST || ''
|
||||
|
||||
const spinner = createConsoleSpinner('服务启动中')
|
||||
|
||||
const checkVersionSubmission =
|
||||
options.checkVersion &&
|
||||
checkVersion().then(({ npmVersion, ourVersion, status }) => {
|
||||
if (status == VERSION_CHECK_RESULT.NOT_LATEST) {
|
||||
logger.info(
|
||||
console.log(
|
||||
`最新版本: ${npmVersion}, 当前版本: ${ourVersion}, 请及时更新`,
|
||||
)
|
||||
}
|
||||
})
|
||||
const constructServerSubmission = constructServer(options.moduleDefs)
|
||||
const constructServerSubmission = consturctServer(options.moduleDefs)
|
||||
|
||||
const [_, app] = await Promise.all([
|
||||
checkVersionSubmission,
|
||||
constructServerSubmission,
|
||||
])
|
||||
|
||||
spinner.stop()
|
||||
|
||||
/** @type {import('express').Express & ExpressExtension} */
|
||||
const appExt = app
|
||||
appExt.server = app.listen(port, host, () => {
|
||||
console.log(`
|
||||
╔═╗╔═╗╦ ╔═╗╔╗╔╦ ╦╔═╗╔╗╔╔═╗╔═╗╔╦╗
|
||||
╠═╣╠═╝║ ║╣ ║║║╠═╣╠═╣║║║║ ║╣ ║║
|
||||
╩ ╩╩ ╩ ╚═╝╝╚╝╩ ╩╩ ╩╝╚╝╚═╝╚═╝═╩╝
|
||||
`)
|
||||
logger.info(`
|
||||
- Server started successfully @ http://${host ? host : 'localhost'}:${port}
|
||||
- Environment: ${process.env.NODE_ENV || 'development'}
|
||||
- Node Version: ${process.version}
|
||||
- Process ID: ${process.pid}`)
|
||||
console.log(`server running @ http://${host ? host : 'localhost'}:${port}`)
|
||||
})
|
||||
|
||||
return appExt
|
||||
|
||||
@@ -4,7 +4,7 @@ const host = global.host || 'http://localhost:3000'
|
||||
describe('测试获取歌词是否正常', () => {
|
||||
it('数据应该有 lrc 字段', (done) => {
|
||||
const qs = {
|
||||
id: 1969519579,
|
||||
id: 347230,
|
||||
realIP: global.cnIp,
|
||||
}
|
||||
|
||||
|
||||
@@ -4,14 +4,13 @@ const host = global.host || 'http://localhost:3000'
|
||||
describe('测试获取歌曲是否正常', () => {
|
||||
it('歌曲的 url 不应该为空', (done) => {
|
||||
const qs = {
|
||||
id: 2709812973,
|
||||
level: 'exhigh',
|
||||
//realip: '125.88.253.199',
|
||||
id: 464315036,
|
||||
br: 999000,
|
||||
realIP: global.cnIp,
|
||||
}
|
||||
|
||||
axios
|
||||
.get(`${host}/song/url/v1`, {
|
||||
.get(`${host}/song/url`, {
|
||||
params: qs,
|
||||
})
|
||||
.then(({ status, data }) => {
|
||||
|
||||
@@ -4,17 +4,17 @@ const host = global.host || 'http://localhost:3000'
|
||||
describe('测试搜索是否正常', () => {
|
||||
it('获取到的数据的 name 应该和搜索关键词一致', (done) => {
|
||||
const qs = {
|
||||
keywords: '拂晓 Proi Proi',
|
||||
keywords: '海阔天空',
|
||||
type: 1,
|
||||
realIP: global.cnIp,
|
||||
}
|
||||
axios
|
||||
.get(`${host}/cloudsearch`, {
|
||||
.get(`${host}/search`, {
|
||||
params: qs,
|
||||
})
|
||||
.then(({ status, data }) => {
|
||||
if (status == 200) {
|
||||
assert(data.result.songs[0].name === '拂晓 Proi Proi')
|
||||
assert(data.result.songs[0].name === '海阔天空')
|
||||
}
|
||||
done()
|
||||
})
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
"target": "ES2015",
|
||||
"module": "commonjs",
|
||||
"experimentalDecorators": true,
|
||||
"ignoreDeprecations": "6.0",
|
||||
"moduleResolution": "node",
|
||||
"lib": [
|
||||
"esnext",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
var url = require('url')
|
||||
var MemoryCache = require('./memory-cache')
|
||||
const logger = require('./logger.js')
|
||||
|
||||
var t = {
|
||||
ms: 1,
|
||||
@@ -155,12 +154,9 @@ function ApiCache() {
|
||||
}
|
||||
|
||||
// add automatic cache clearing from duration, includes max limit on setTimeout
|
||||
timers[key] = setTimeout(
|
||||
function () {
|
||||
instance.clear(key, true)
|
||||
},
|
||||
Math.min(duration, 2147483647),
|
||||
)
|
||||
timers[key] = setTimeout(function () {
|
||||
instance.clear(key, true)
|
||||
}, Math.min(duration, 2147483647))
|
||||
}
|
||||
|
||||
function accumulateContent(res, content) {
|
||||
@@ -353,7 +349,7 @@ function ApiCache() {
|
||||
try {
|
||||
redis.del(key)
|
||||
} catch (err) {
|
||||
logger.info('[apicache] error in redis.del("' + key + '")')
|
||||
console.log('[apicache] error in redis.del("' + key + '")')
|
||||
}
|
||||
}
|
||||
index.all = index.all.filter(doesntMatch(key))
|
||||
@@ -377,7 +373,7 @@ function ApiCache() {
|
||||
try {
|
||||
redis.del(target)
|
||||
} catch (err) {
|
||||
logger.info('[apicache] error in redis.del("' + target + '")')
|
||||
console.log('[apicache] error in redis.del("' + target + '")')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -408,7 +404,7 @@ function ApiCache() {
|
||||
try {
|
||||
redis.del(key)
|
||||
} catch (err) {
|
||||
logger.info('[apicache] error in redis.del("' + key + '")')
|
||||
console.log('[apicache] error in redis.del("' + key + '")')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||