mirror of
https://github.com/NeteaseCloudMusicApiEnhanced/api-enhanced.git
synced 2026-03-21 19:13:10 +00:00
Compare commits
30 Commits
98ec6d091b
...
a4ee3eadfc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a4ee3eadfc | ||
| b027aca40a | |||
|
|
b1729f19aa | ||
|
|
755489546b | ||
|
|
92df6e13a0 | ||
|
|
1ad8ab088f | ||
|
|
a09f126ab2 | ||
|
|
872bae1b43 | ||
|
|
83c527af01 | ||
|
|
84efe5d758 | ||
|
|
cfa475bf94 | ||
|
|
4087bafd7d | ||
|
|
8951e32a0e | ||
|
|
33ccc83615 | ||
|
|
2d6173b2aa | ||
|
|
ba7d1a8574 | ||
|
|
26d55255e0 | ||
|
|
4489f10f63 | ||
| 2047bdfa65 | |||
| bc9d7b949f | |||
| f08f192328 | |||
| b04b9d51fb | |||
|
|
c91f5f3714 | ||
| 7fceec12e4 | |||
|
|
1df147edab | ||
|
|
6d1f75f229 | ||
| 2a0b54e442 | |||
|
|
a19418bbb6 | ||
| ce51da9150 | |||
|
|
27002d7665 |
@ -7,9 +7,9 @@
|
|||||||
!/app.js
|
!/app.js
|
||||||
!/server.js
|
!/server.js
|
||||||
!/package.json
|
!/package.json
|
||||||
!/package-lock.json
|
|
||||||
!/index.js
|
!/index.js
|
||||||
!/generateConfig.js
|
!/generateConfig.js
|
||||||
!/main.js
|
!/main.js
|
||||||
!/data
|
!/data
|
||||||
!/.env
|
!/.env
|
||||||
|
!/pnpm-lock.yaml
|
||||||
2
.github/dependabot.yml
vendored
2
.github/dependabot.yml
vendored
@ -16,7 +16,7 @@ updates:
|
|||||||
# 自动合并小版本更新的 PR (可选)
|
# 自动合并小版本更新的 PR (可选)
|
||||||
# 可以设置针对 patch 或 minor 版本更新自动合并
|
# 可以设置针对 patch 或 minor 版本更新自动合并
|
||||||
commit-message:
|
commit-message:
|
||||||
prefix: "chore(packages):" # PR 合并提交信息的前缀
|
prefix: "build(packages):" # PR 合并提交信息的前缀
|
||||||
|
|
||||||
# 更新 GitHub Actions 依赖
|
# 更新 GitHub Actions 依赖
|
||||||
- package-ecosystem: "github-actions"
|
- package-ecosystem: "github-actions"
|
||||||
|
|||||||
16
.github/workflows/build-and-pr.yml
vendored
16
.github/workflows/build-and-pr.yml
vendored
@ -3,7 +3,7 @@ name: Build and Create PR
|
|||||||
on:
|
on:
|
||||||
workflow_dispatch: # 手动触发
|
workflow_dispatch: # 手动触发
|
||||||
push:
|
push:
|
||||||
branches: [main, pr/*]
|
branches: [main]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@ -27,10 +27,10 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: '18'
|
node-version: '18'
|
||||||
|
|
||||||
@ -46,7 +46,7 @@ jobs:
|
|||||||
PKG_TARGET: ${{ matrix.target }}
|
PKG_TARGET: ${{ matrix.target }}
|
||||||
|
|
||||||
- name: Upload artifacts
|
- name: Upload artifacts
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: app-${{ matrix.platform }}
|
name: app-${{ matrix.platform }}
|
||||||
path: ${{ matrix.output }}
|
path: ${{ matrix.output }}
|
||||||
@ -59,10 +59,10 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Download all artifacts
|
- name: Download all artifacts
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v7
|
||||||
with:
|
with:
|
||||||
path: precompiled
|
path: precompiled
|
||||||
|
|
||||||
@ -70,7 +70,7 @@ jobs:
|
|||||||
run: ls -R
|
run: ls -R
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v6
|
||||||
|
|
||||||
- name: Create new branch and commit
|
- name: Create new branch and commit
|
||||||
run: |
|
run: |
|
||||||
@ -95,7 +95,7 @@ jobs:
|
|||||||
git push origin $BRANCH_NAME
|
git push origin $BRANCH_NAME
|
||||||
|
|
||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
uses: actions/github-script@v7
|
uses: actions/github-script@v8
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const { data: pullRequest } = await github.rest.pulls.create({
|
const { data: pullRequest } = await github.rest.pulls.create({
|
||||||
|
|||||||
21
CHANGELOG.MD
21
CHANGELOG.MD
@ -1,6 +1,25 @@
|
|||||||
# 更新日志
|
# 更新日志
|
||||||
|
|
||||||
## 二开作者注: 这些`commit`记录为原版网易云音乐API的记录, 本项目不会对其进行添加以及修改
|
### 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
|
### 4.25.0 | 2024.11.16
|
||||||
- feat: 增加副歌时间、相关歌单推荐接口,原有相关歌单接口已废弃;fix: 将部分易盾白名单接口替换为eapi [#30](https://gitlab.com/Binaryify/neteasecloudmusicapi/-/merge_requests/30)
|
- feat: 增加副歌时间、相关歌单推荐接口,原有相关歌单接口已废弃;fix: 将部分易盾白名单接口替换为eapi [#30](https://gitlab.com/Binaryify/neteasecloudmusicapi/-/merge_requests/30)
|
||||||
|
|||||||
13
README.MD
13
README.MD
@ -101,7 +101,7 @@ $ sudo docker run -d -p 3000:3000 ncm-api
|
|||||||
## 3. 环境变量
|
## 3. 环境变量
|
||||||
|
|
||||||
| 变量名 | 默认值 | 说明 |
|
| 变量名 | 默认值 | 说明 |
|
||||||
| -------------------------- | ------------------------------------ | ------------------------------------------------------------------------------ |
|
|----------------------------|--------------------------------------|----------------------------------------------------|
|
||||||
| **CORS_ALLOW_ORIGIN** | `*` | 允许跨域请求的域名。若需要限制,请指定具体域名(例如 `https://example.com`)。 |
|
| **CORS_ALLOW_ORIGIN** | `*` | 允许跨域请求的域名。若需要限制,请指定具体域名(例如 `https://example.com`)。 |
|
||||||
| **ENABLE_PROXY** | `false` | 是否启用反向代理功能。 |
|
| **ENABLE_PROXY** | `false` | 是否启用反向代理功能。 |
|
||||||
| **PROXY_URL** | `https://your-proxy-url.com/?proxy=` | 代理服务地址。仅当 `ENABLE_PROXY=true` 时生效。 |
|
| **PROXY_URL** | `https://your-proxy-url.com/?proxy=` | 代理服务地址。仅当 `ENABLE_PROXY=true` 时生效。 |
|
||||||
@ -183,6 +183,15 @@ pnpm test
|
|||||||
|
|
||||||
- 欢迎提交 PR、Issue 参与维护
|
- 欢迎提交 PR、Issue 参与维护
|
||||||
|
|
||||||
|
## 最近更新日志
|
||||||
|
### 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地址日志输出以提升隐私保护
|
||||||
|
|
||||||
### 致谢
|
### 致谢
|
||||||
|
|
||||||
原作者 [Binaryify/NeteaseCloudMusicApi](https://github.com/binaryify/NeteaseCloudMusicApi) 项目为本项目基础 (该项目在`npmjs`网站上仍持续维护, 但 github 仓库已不再更新)
|
原作者 [Binaryify/NeteaseCloudMusicApi](https://github.com/binaryify/NeteaseCloudMusicApi) 项目为本项目基础 (该项目在`npmjs`网站上仍持续维护, 但 github 仓库已不再更新)
|
||||||
@ -200,7 +209,7 @@ pnpm test
|
|||||||
### SDK 生态
|
### SDK 生态
|
||||||
|
|
||||||
| 语言 | 作者 | 地址 | 类型 |
|
| 语言 | 作者 | 地址 | 类型 |
|
||||||
| ------ | ------------------------------------------- | ---------------------------------------------------------------------------------------- | ------ |
|
|--------|---------------------------------------------|------------------------------------------------------------------------------------------|-----|
|
||||||
| Java | [JackuXL](https://github.com/JackuXL) | [NeteaseCloudMusicApi-SDK](https://github.com/JackuXL/NeteaseCloudMusicApi-SDK) | 第三方 |
|
| Java | [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 | 第三方 |
|
| 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) | 第三方 |
|
| Python | [盧瞳](https://github.com/2061360308) | [NeteaseCloudMusic_PythonSDK](https://github.com/2061360308/NeteaseCloudMusic_PythonSDK) | 第三方 |
|
||||||
|
|||||||
@ -1,26 +0,0 @@
|
|||||||
开始IP,结束IP,IP个数,位置
|
|
||||||
1.0.1.0 ,1.0.3.255 ,768,福州
|
|
||||||
1.0.8.0 ,1.0.15.255 ,2048,广州
|
|
||||||
1.0.32.0 ,1.0.63.255 ,8192,广州
|
|
||||||
1.1.0.0 ,1.1.0.255 ,256,福州
|
|
||||||
1.1.2.0 ,1.1.63.255 ,15872,广州
|
|
||||||
1.2.0.0 ,1.2.2.255 ,768,北京
|
|
||||||
1.2.4.0 ,1.2.127.255 ,31744,广州
|
|
||||||
1.3.0.0 ,1.3.255.255 ,65536,广州
|
|
||||||
1.4.1.0 ,1.4.127.255 ,32512,广州
|
|
||||||
1.8.0.0 ,1.8.255.255 ,65536,北京
|
|
||||||
1.10.0.0 ,1.10.9.255 ,2560,福州
|
|
||||||
1.10.11.0 ,1.10.127.255 ,29952,广州
|
|
||||||
1.12.0.0 ,1.15.255.255 ,262144,上海
|
|
||||||
1.18.128.0 ,1.18.128.255 ,256,北京
|
|
||||||
1.24.0.0 ,1.31.255.255 ,524288,赤峰
|
|
||||||
1.45.0.0 ,1.45.255.255 ,65536,北京
|
|
||||||
1.48.0.0 ,1.51.255.255 ,262144,济南
|
|
||||||
1.56.0.0 ,1.63.255.255 ,524288,伊春
|
|
||||||
1.68.0.0 ,1.71.255.255 ,262144,忻州
|
|
||||||
1.80.0.0 ,1.95.255.255 ,1048576,北京
|
|
||||||
1.116.0.0 ,1.117.255.255 ,131072,上海
|
|
||||||
1.119.0.0 ,1.119.255.255 ,65536,北京
|
|
||||||
1.180.0.0 ,1.185.255.255 ,393216,桂林
|
|
||||||
1.188.0.0 ,1.199.255.255 ,786432,洛阳
|
|
||||||
1.202.0.0 ,1.207.255.255 ,393216,铜仁
|
|
||||||
|
4147
data/china_ip_ranges.txt
Normal file
4147
data/china_ip_ranges.txt
Normal file
File diff suppressed because it is too large
Load Diff
@ -16,7 +16,7 @@ const compat = new FlatCompat({
|
|||||||
module.exports = defineConfig([
|
module.exports = defineConfig([
|
||||||
{
|
{
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
ecmaVersion: 2018,
|
ecmaVersion: 2020,
|
||||||
sourceType: 'module',
|
sourceType: 'module',
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
parser: 'babel-eslint',
|
parser: 'babel-eslint',
|
||||||
|
|||||||
2
interface.d.ts
vendored
2
interface.d.ts
vendored
@ -1715,6 +1715,8 @@ export function nickname_check(
|
|||||||
|
|
||||||
export function musician_tasks_new(params: RequestBaseConfig): Promise<Response>
|
export function musician_tasks_new(params: RequestBaseConfig): Promise<Response>
|
||||||
|
|
||||||
|
export function musician_vip_tasks(params: RequestBaseConfig): Promise<Response>
|
||||||
|
|
||||||
export function playlist_update_playcount(
|
export function playlist_update_playcount(
|
||||||
params: {
|
params: {
|
||||||
id?: number | string
|
id?: number | string
|
||||||
|
|||||||
147
module/cloud.js
147
module/cloud.js
@ -1,25 +1,26 @@
|
|||||||
const uploadPlugin = require('../plugins/songUpload')
|
const uploadPlugin = require('../plugins/songUpload')
|
||||||
const md5 = require('md5')
|
|
||||||
const createOption = require('../util/option.js')
|
const createOption = require('../util/option.js')
|
||||||
const logger = require('../util/logger.js')
|
const logger = require('../util/logger.js')
|
||||||
|
const {
|
||||||
|
isTempFile,
|
||||||
|
getFileSize,
|
||||||
|
getFileMd5,
|
||||||
|
cleanupTempFile,
|
||||||
|
getFileExtension,
|
||||||
|
sanitizeFilename,
|
||||||
|
} = require('../util/fileHelper')
|
||||||
|
|
||||||
let mm
|
let mm
|
||||||
module.exports = async (query, request) => {
|
module.exports = async (query, request) => {
|
||||||
mm = require('music-metadata')
|
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(
|
query.songFile.name = Buffer.from(query.songFile.name, 'latin1').toString(
|
||||||
'utf-8',
|
'utf-8',
|
||||||
)
|
)
|
||||||
const filename = query.songFile.name
|
const ext = getFileExtension(query.songFile.name)
|
||||||
.replace('.' + ext, '')
|
const filename = sanitizeFilename(query.songFile.name)
|
||||||
.replace(/\s/g, '')
|
|
||||||
.replace(/\./g, '_')
|
|
||||||
const bitrate = 999000
|
const bitrate = 999000
|
||||||
|
|
||||||
if (!query.songFile) {
|
if (!query.songFile) {
|
||||||
return Promise.reject({
|
return Promise.reject({
|
||||||
status: 500,
|
status: 500,
|
||||||
@ -29,69 +30,50 @@ module.exports = async (query, request) => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (!query.songFile.md5) {
|
|
||||||
// 命令行上传没有md5和size信息,需要填充
|
const useTemp = isTempFile(query.songFile)
|
||||||
query.songFile.md5 = md5(query.songFile.data)
|
let fileSize = await getFileSize(query.songFile)
|
||||||
query.songFile.size = query.songFile.data.byteLength
|
let fileMd5 = await getFileMd5(query.songFile)
|
||||||
}
|
|
||||||
|
query.songFile.md5 = fileMd5
|
||||||
|
query.songFile.size = fileSize
|
||||||
|
|
||||||
|
try {
|
||||||
const res = await request(
|
const res = await request(
|
||||||
`/api/cloud/upload/check`,
|
`/api/cloud/upload/check`,
|
||||||
{
|
{
|
||||||
bitrate: String(bitrate),
|
bitrate: String(bitrate),
|
||||||
ext: '',
|
ext: '',
|
||||||
length: query.songFile.size,
|
length: fileSize,
|
||||||
md5: query.songFile.md5,
|
md5: fileMd5,
|
||||||
songId: '0',
|
songId: '0',
|
||||||
version: 1,
|
version: 1,
|
||||||
},
|
},
|
||||||
createOption(query),
|
createOption(query),
|
||||||
)
|
)
|
||||||
|
|
||||||
let artist = ''
|
let artist = ''
|
||||||
let album = ''
|
let album = ''
|
||||||
let songName = ''
|
let songName = ''
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const metadata = await mm.parseBuffer(
|
let metadata
|
||||||
|
if (useTemp) {
|
||||||
|
metadata = await mm.parseFile(query.songFile.tempFilePath)
|
||||||
|
} else {
|
||||||
|
metadata = await mm.parseBuffer(
|
||||||
query.songFile.data,
|
query.songFile.data,
|
||||||
query.songFile.mimetype,
|
query.songFile.mimetype,
|
||||||
)
|
)
|
||||||
|
}
|
||||||
const info = metadata.common
|
const info = metadata.common
|
||||||
|
if (info.title) songName = info.title
|
||||||
if (info.title) {
|
if (info.album) album = info.album
|
||||||
songName = info.title
|
if (info.artist) artist = info.artist
|
||||||
}
|
|
||||||
if (info.album) {
|
|
||||||
album = info.album
|
|
||||||
}
|
|
||||||
if (info.artist) {
|
|
||||||
artist = info.artist
|
|
||||||
}
|
|
||||||
// if (metadata.native.ID3v1) {
|
|
||||||
// metadata.native.ID3v1.forEach((item) => {
|
|
||||||
// // logger.info(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
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// // logger.info({
|
|
||||||
// // songName,
|
|
||||||
// // album,
|
|
||||||
// // songName,
|
|
||||||
// // })
|
|
||||||
// }
|
|
||||||
// logger.info({
|
|
||||||
// songName,
|
|
||||||
// album,
|
|
||||||
// songName,
|
|
||||||
// })
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.info(error)
|
logger.info('元数据解析错误:', error.message)
|
||||||
}
|
}
|
||||||
|
|
||||||
const tokenRes = await request(
|
const tokenRes = await request(
|
||||||
`/api/nos/token/alloc`,
|
`/api/nos/token/alloc`,
|
||||||
{
|
{
|
||||||
@ -101,20 +83,40 @@ module.exports = async (query, request) => {
|
|||||||
local: false,
|
local: false,
|
||||||
nos_product: 3,
|
nos_product: 3,
|
||||||
type: 'audio',
|
type: 'audio',
|
||||||
md5: query.songFile.md5,
|
md5: fileMd5,
|
||||||
},
|
},
|
||||||
createOption(query),
|
createOption(query),
|
||||||
)
|
)
|
||||||
|
|
||||||
if (res.body.needUpload) {
|
if (!tokenRes.body.result || !tokenRes.body.result.resourceId) {
|
||||||
const uploadInfo = await uploadPlugin(query, request)
|
logger.error('Token分配失败:', tokenRes.body)
|
||||||
// logger.info('uploadInfo', uploadInfo.body.result.resourceId)
|
return Promise.reject({
|
||||||
|
status: 500,
|
||||||
|
body: {
|
||||||
|
code: 500,
|
||||||
|
msg: '获取上传token失败',
|
||||||
|
detail: tokenRes.body,
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
// logger.info(tokenRes.body.result)
|
|
||||||
|
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('文件已存在,跳过上传')
|
||||||
|
}
|
||||||
|
|
||||||
const res2 = await request(
|
const res2 = await request(
|
||||||
`/api/upload/cloud/info/v2`,
|
`/api/upload/cloud/info/v2`,
|
||||||
{
|
{
|
||||||
md5: query.songFile.md5,
|
md5: fileMd5,
|
||||||
songid: res.body.songId,
|
songid: res.body.songId,
|
||||||
filename: query.songFile.name,
|
filename: query.songFile.name,
|
||||||
song: songName || filename,
|
song: songName || filename,
|
||||||
@ -125,8 +127,19 @@ module.exports = async (query, request) => {
|
|||||||
},
|
},
|
||||||
createOption(query),
|
createOption(query),
|
||||||
)
|
)
|
||||||
// logger.info({ res2, privateCloud: res2.body.privateCloud })
|
|
||||||
// logger.info(res.body.songId, 'songid')
|
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(
|
const res3 = await request(
|
||||||
`/api/cloud/pub/v2`,
|
`/api/cloud/pub/v2`,
|
||||||
{
|
{
|
||||||
@ -134,14 +147,18 @@ module.exports = async (query, request) => {
|
|||||||
},
|
},
|
||||||
createOption(query),
|
createOption(query),
|
||||||
)
|
)
|
||||||
// logger.info({ res3 })
|
|
||||||
return {
|
return {
|
||||||
status: 200,
|
status: 200,
|
||||||
body: {
|
body: {
|
||||||
...res.body,
|
...res.body,
|
||||||
...res3.body,
|
...res3.body,
|
||||||
// ...uploadInfo,
|
|
||||||
},
|
},
|
||||||
cookie: res.cookie,
|
cookie: res.cookie,
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
if (useTemp) {
|
||||||
|
await cleanupTempFile(query.songFile.tempFilePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
72
module/cloud_upload_complete.js
Normal file
72
module/cloud_upload_complete.js
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
111
module/cloud_upload_token.js
Normal file
111
module/cloud_upload_token.js
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
11
module/musician_vip_tasks.js
Normal file
11
module/musician_vip_tasks.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
// 获取音乐人任务
|
||||||
|
|
||||||
|
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'),
|
||||||
|
)
|
||||||
|
}
|
||||||
14
module/user_playlist_collect.js
Normal file
14
module/user_playlist_collect.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
// 获取用户的收藏歌单列表
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
limit: query.limit || '100',
|
||||||
|
offset: query.offset || '0',
|
||||||
|
userId: query.uid,
|
||||||
|
isWebview: 'true',
|
||||||
|
includeRedHeart: 'true',
|
||||||
|
includeTop: 'true',
|
||||||
|
}
|
||||||
|
return request(`/api/user/playlist/collect`, data, createOption(query))
|
||||||
|
}
|
||||||
14
module/user_playlist_create.js
Normal file
14
module/user_playlist_create.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
// 获取用户的创建歌单列表
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
limit: query.limit || '100',
|
||||||
|
offset: query.offset || '0',
|
||||||
|
userId: query.uid,
|
||||||
|
isWebview: 'true',
|
||||||
|
includeRedHeart: 'true',
|
||||||
|
includeTop: 'true',
|
||||||
|
}
|
||||||
|
return request(`/api/user/playlist/create`, data, createOption(query))
|
||||||
|
}
|
||||||
@ -1,25 +1,26 @@
|
|||||||
const { default: axios } = require('axios')
|
const { default: axios } = require('axios')
|
||||||
|
const fs = require('fs')
|
||||||
var xml2js = require('xml2js')
|
var xml2js = require('xml2js')
|
||||||
|
|
||||||
const createOption = require('../util/option.js')
|
const createOption = require('../util/option.js')
|
||||||
var parser = new xml2js.Parser(/* options */)
|
const { getFileExtension, readFileChunk } = require('../util/fileHelper')
|
||||||
|
|
||||||
|
var parser = new xml2js.Parser()
|
||||||
|
|
||||||
function createDupkey() {
|
function createDupkey() {
|
||||||
// 格式:3b443c7c-a87f-468d-ba38-46d407aaf23a
|
|
||||||
var s = []
|
var s = []
|
||||||
var hexDigits = '0123456789abcdef'
|
var hexDigits = '0123456789abcdef'
|
||||||
for (var i = 0; i < 36; i++) {
|
for (var i = 0; i < 36; i++) {
|
||||||
s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1)
|
s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1)
|
||||||
}
|
}
|
||||||
s[14] = '4' // bits 12-15 of the time_hi_and_version field to 0010
|
s[14] = '4'
|
||||||
s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1) // bits 6-7 of the clock_seq_hi_and_reserved to 01
|
s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1)
|
||||||
s[8] = s[13] = s[18] = s[23] = '-'
|
s[8] = s[13] = s[18] = s[23] = '-'
|
||||||
return s.join('')
|
return s.join('')
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = async (query, request) => {
|
module.exports = async (query, request) => {
|
||||||
let ext = 'mp3'
|
const ext = getFileExtension(query.songFile.name)
|
||||||
if (query.songFile.name.indexOf('flac') > -1) {
|
|
||||||
ext = 'flac'
|
|
||||||
}
|
|
||||||
const filename =
|
const filename =
|
||||||
query.songName ||
|
query.songName ||
|
||||||
query.songFile.name
|
query.songFile.name
|
||||||
@ -50,43 +51,58 @@ module.exports = async (query, request) => {
|
|||||||
createOption(query, 'weapi'),
|
createOption(query, 'weapi'),
|
||||||
)
|
)
|
||||||
|
|
||||||
const objectKey = tokenRes.body.result.objectKey.replace('/', '%2F')
|
const objectKey = tokenRes.body.result.objectKey.replace(/\//g, '%2F')
|
||||||
const docId = tokenRes.body.result.docId
|
const docId = tokenRes.body.result.docId
|
||||||
const res = await axios({
|
const res = await axios({
|
||||||
method: 'post',
|
method: 'post',
|
||||||
url: `https://ymusic.nos-hz.163yun.com/${objectKey}?uploads`,
|
url: `https://ymusic.nos-hz.163yun.com/${objectKey}?uploads`,
|
||||||
headers: {
|
headers: {
|
||||||
'x-nos-token': tokenRes.body.result.token,
|
'x-nos-token': tokenRes.body.result.token,
|
||||||
'X-Nos-Meta-Content-Type': 'audio/mpeg',
|
'X-Nos-Meta-Content-Type': query.songFile.mimetype || 'audio/mpeg',
|
||||||
},
|
},
|
||||||
data: null,
|
data: null,
|
||||||
})
|
})
|
||||||
// return xml
|
|
||||||
const res2 = await parser.parseStringPromise(res.data)
|
const res2 = await parser.parseStringPromise(res.data)
|
||||||
|
|
||||||
const fileSize = query.songFile.data.length
|
const useTempFile = !!query.songFile.tempFilePath
|
||||||
const blockSize = 10 * 1024 * 1024 // 10MB
|
let fileSize = query.songFile.size
|
||||||
|
|
||||||
|
if (useTempFile) {
|
||||||
|
const stats = await fs.promises.stat(query.songFile.tempFilePath)
|
||||||
|
fileSize = stats.size
|
||||||
|
}
|
||||||
|
|
||||||
|
const blockSize = 10 * 1024 * 1024
|
||||||
let offset = 0
|
let offset = 0
|
||||||
let blockIndex = 1
|
let blockIndex = 1
|
||||||
|
|
||||||
let etags = []
|
let etags = []
|
||||||
|
|
||||||
while (offset < fileSize) {
|
while (offset < fileSize) {
|
||||||
const chunk = query.songFile.data.slice(
|
let chunk
|
||||||
|
if (useTempFile) {
|
||||||
|
chunk = await readFileChunk(
|
||||||
|
query.songFile.tempFilePath,
|
||||||
|
offset,
|
||||||
|
Math.min(blockSize, fileSize - offset),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
chunk = query.songFile.data.slice(
|
||||||
offset,
|
offset,
|
||||||
Math.min(offset + blockSize, fileSize),
|
Math.min(offset + blockSize, fileSize),
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const res3 = await axios({
|
const res3 = await axios({
|
||||||
method: 'put',
|
method: 'put',
|
||||||
url: `https://ymusic.nos-hz.163yun.com/${objectKey}?partNumber=${blockIndex}&uploadId=${res2.InitiateMultipartUploadResult.UploadId[0]}`,
|
url: `https://ymusic.nos-hz.163yun.com/${objectKey}?partNumber=${blockIndex}&uploadId=${res2.InitiateMultipartUploadResult.UploadId[0]}`,
|
||||||
headers: {
|
headers: {
|
||||||
'x-nos-token': tokenRes.body.result.token,
|
'x-nos-token': tokenRes.body.result.token,
|
||||||
'Content-Type': 'audio/mpeg',
|
'Content-Type': query.songFile.mimetype || 'audio/mpeg',
|
||||||
},
|
},
|
||||||
data: chunk,
|
data: chunk,
|
||||||
})
|
})
|
||||||
// get etag
|
|
||||||
const etag = res3.headers.etag
|
const etag = res3.headers.etag
|
||||||
etags.push(etag)
|
etags.push(etag)
|
||||||
offset += blockSize
|
offset += blockSize
|
||||||
@ -101,19 +117,17 @@ module.exports = async (query, request) => {
|
|||||||
}
|
}
|
||||||
completeStr += '</CompleteMultipartUpload>'
|
completeStr += '</CompleteMultipartUpload>'
|
||||||
|
|
||||||
// 文件处理
|
|
||||||
await axios({
|
await axios({
|
||||||
method: 'post',
|
method: 'post',
|
||||||
url: `https://ymusic.nos-hz.163yun.com/${objectKey}?uploadId=${res2.InitiateMultipartUploadResult.UploadId[0]}`,
|
url: `https://ymusic.nos-hz.163yun.com/${objectKey}?uploadId=${res2.InitiateMultipartUploadResult.UploadId[0]}`,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'text/plain;charset=UTF-8',
|
'Content-Type': 'text/plain;charset=UTF-8',
|
||||||
'X-Nos-Meta-Content-Type': 'audio/mpeg',
|
'X-Nos-Meta-Content-Type': query.songFile.mimetype || 'audio/mpeg',
|
||||||
'x-nos-token': tokenRes.body.result.token,
|
'x-nos-token': tokenRes.body.result.token,
|
||||||
},
|
},
|
||||||
data: completeStr,
|
data: completeStr,
|
||||||
})
|
})
|
||||||
|
|
||||||
// preCheck
|
|
||||||
await request(
|
await request(
|
||||||
`/api/voice/workbench/voice/batch/upload/preCheck`,
|
`/api/voice/workbench/voice/batch/upload/preCheck`,
|
||||||
{
|
{
|
||||||
|
|||||||
38
package.json
38
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@neteasecloudmusicapienhanced/api",
|
"name": "@neteasecloudmusicapienhanced/api",
|
||||||
"version": "4.29.21",
|
"version": "4.30.1",
|
||||||
"description": "全网最全的网易云音乐API接口 || A revival project for NeteaseCloudMusicApi Node.js Services (Half Refactor & Enhanced) || 网易云音乐 API 备份 + 增强 || 本项目自原版v4.28.0版本后开始自行维护",
|
"description": "全网最全的网易云音乐API接口 || A revival project for NeteaseCloudMusicApi Node.js Services (Half Refactor & Enhanced) || 网易云音乐 API 备份 + 增强 || 本项目自原版v4.28.0版本后开始自行维护",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "nodemon app.js",
|
"dev": "nodemon app.js",
|
||||||
@ -20,7 +20,6 @@
|
|||||||
"node_modules/axios",
|
"node_modules/axios",
|
||||||
"node_modules/express",
|
"node_modules/express",
|
||||||
"node_modules/express-fileupload",
|
"node_modules/express-fileupload",
|
||||||
"node_modules/md5",
|
|
||||||
"node_modules/music-metadata",
|
"node_modules/music-metadata",
|
||||||
"node_modules/pac-proxy-agent",
|
"node_modules/pac-proxy-agent",
|
||||||
"node_modules/qrcode",
|
"node_modules/qrcode",
|
||||||
@ -66,14 +65,13 @@
|
|||||||
"data"
|
"data"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@neteasecloudmusicapienhanced/unblockmusic-utils": "^0.1.3",
|
"@neteasecloudmusicapienhanced/unblockmusic-utils": "^0.2.2",
|
||||||
"axios": "^1.13.4",
|
"axios": "^1.13.5",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.4",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
"express-fileupload": "^1.5.2",
|
"express-fileupload": "^1.5.2",
|
||||||
"md5": "^2.3.0",
|
"music-metadata": "^11.12.0",
|
||||||
"music-metadata": "^11.11.1",
|
|
||||||
"node-forge": "^1.3.3",
|
"node-forge": "^1.3.3",
|
||||||
"pac-proxy-agent": "^7.2.0",
|
"pac-proxy-agent": "^7.2.0",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
@ -89,21 +87,21 @@
|
|||||||
"@types/express-fileupload": "^1.5.1",
|
"@types/express-fileupload": "^1.5.1",
|
||||||
"@types/mocha": "^10.0.10",
|
"@types/mocha": "^10.0.10",
|
||||||
"@types/node": "25.0.9",
|
"@types/node": "25.0.9",
|
||||||
"@typescript-eslint/eslint-plugin": "8.46.3",
|
"@typescript-eslint/eslint-plugin": "^8.46.3",
|
||||||
"@typescript-eslint/parser": "8.53.0",
|
"@typescript-eslint/parser": "^8.53.0",
|
||||||
"eslint": "9.39.0",
|
"eslint": "^9.39.0",
|
||||||
"eslint-config-prettier": "10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"eslint-plugin-html": "8.1.3",
|
"eslint-plugin-html": "^8.1.3",
|
||||||
"eslint-plugin-prettier": "5.5.5",
|
"eslint-plugin-prettier": "^5.5.5",
|
||||||
"globals": "^16.5.0",
|
"globals": "^16.5.0",
|
||||||
"husky": "9.1.7",
|
"husky": "^9.1.7",
|
||||||
"intelli-espower-loader": "1.1.0",
|
"intelli-espower-loader": "^1.1.0",
|
||||||
"lint-staged": "16.2.7",
|
"lint-staged": "^16.2.7",
|
||||||
"mocha": "11.7.5",
|
"mocha": "^11.7.5",
|
||||||
"nodemon": "^3.1.11",
|
"nodemon": "^3.1.11",
|
||||||
"pkg": "^5.8.1",
|
"pkg": "^5.8.1",
|
||||||
"power-assert": "1.6.1",
|
"power-assert": "^1.6.1",
|
||||||
"prettier": "3.8.1",
|
"prettier": "^3.7.4",
|
||||||
"typescript": "5.9.3"
|
"typescript": "^5.9.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,20 +1,17 @@
|
|||||||
const { default: axios } = require('axios')
|
const { default: axios } = require('axios')
|
||||||
const createOption = require('../util/option.js')
|
const createOption = require('../util/option.js')
|
||||||
const logger = require('../util/logger.js')
|
const logger = require('../util/logger.js')
|
||||||
|
const {
|
||||||
|
getUploadData,
|
||||||
|
getFileExtension,
|
||||||
|
sanitizeFilename,
|
||||||
|
} = require('../util/fileHelper')
|
||||||
|
|
||||||
module.exports = async (query, request) => {
|
module.exports = async (query, request) => {
|
||||||
let ext = 'mp3'
|
const ext = getFileExtension(query.songFile.name)
|
||||||
// if (query.songFile.name.indexOf('flac') > -1) {
|
const filename = sanitizeFilename(query.songFile.name)
|
||||||
// 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'
|
const bucket = 'jd-musicrep-privatecloud-audio-public'
|
||||||
// 获取key和token
|
|
||||||
const tokenRes = await request(
|
const tokenRes = await request(
|
||||||
`/api/nos/token/alloc`,
|
`/api/nos/token/alloc`,
|
||||||
{
|
{
|
||||||
@ -29,31 +26,82 @@ module.exports = async (query, request) => {
|
|||||||
createOption(query, 'weapi'),
|
createOption(query, 'weapi'),
|
||||||
)
|
)
|
||||||
|
|
||||||
// 上传
|
if (!tokenRes.body.result || !tokenRes.body.result.objectKey) {
|
||||||
const objectKey = tokenRes.body.result.objectKey.replace('/', '%2F')
|
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
|
||||||
try {
|
try {
|
||||||
const lbs = (
|
lbs = (
|
||||||
await axios({
|
await axios({
|
||||||
method: 'get',
|
method: 'get',
|
||||||
url: `https://wanproxy.127.net/lbs?version=1.0&bucketname=${bucket}`,
|
url: `https://wanproxy.127.net/lbs?version=1.0&bucketname=${bucket}`,
|
||||||
|
timeout: 10000,
|
||||||
})
|
})
|
||||||
).data
|
).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({
|
await axios({
|
||||||
method: 'post',
|
method: 'post',
|
||||||
url: `${lbs.upload[0]}/${bucket}/${objectKey}?offset=0&complete=true&version=1.0`,
|
url: `${lbs.upload[0]}/${bucket}/${objectKey}?offset=0&complete=true&version=1.0`,
|
||||||
headers: {
|
headers: {
|
||||||
'x-nos-token': tokenRes.body.result.token,
|
'x-nos-token': tokenRes.body.result.token,
|
||||||
'Content-MD5': query.songFile.md5,
|
'Content-MD5': query.songFile.md5,
|
||||||
'Content-Type': 'audio/mpeg',
|
'Content-Type': query.songFile.mimetype || 'audio/mpeg',
|
||||||
'Content-Length': String(query.songFile.size),
|
'Content-Length': String(query.songFile.size),
|
||||||
},
|
},
|
||||||
data: query.songFile.data,
|
data: getUploadData(query.songFile),
|
||||||
maxContentLength: Infinity,
|
maxContentLength: Infinity,
|
||||||
maxBodyLength: Infinity,
|
maxBodyLength: Infinity,
|
||||||
|
timeout: 300000,
|
||||||
})
|
})
|
||||||
|
logger.info('上传成功:', filename)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.info('error', error.response)
|
logger.error('上传失败:', {
|
||||||
throw error.response
|
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,
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...tokenRes,
|
...tokenRes,
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
const { default: axios } = require('axios')
|
const { default: axios } = require('axios')
|
||||||
const createOption = require('../util/option.js')
|
const createOption = require('../util/option.js')
|
||||||
|
const { getUploadData } = require('../util/fileHelper')
|
||||||
|
|
||||||
module.exports = async (query, request) => {
|
module.exports = async (query, request) => {
|
||||||
const data = {
|
const data = {
|
||||||
bucket: 'yyimgs',
|
bucket: 'yyimgs',
|
||||||
@ -10,27 +12,23 @@ module.exports = async (query, request) => {
|
|||||||
return_body: `{"code":200,"size":"$(ObjectSize)"}`,
|
return_body: `{"code":200,"size":"$(ObjectSize)"}`,
|
||||||
type: 'other',
|
type: 'other',
|
||||||
}
|
}
|
||||||
// 获取key和token
|
|
||||||
const res = await request(
|
const res = await request(
|
||||||
`/api/nos/token/alloc`,
|
`/api/nos/token/alloc`,
|
||||||
data,
|
data,
|
||||||
createOption(query, 'weapi'),
|
createOption(query, 'weapi'),
|
||||||
)
|
)
|
||||||
// 上传图片
|
|
||||||
const res2 = await axios({
|
const res2 = await axios({
|
||||||
method: 'post',
|
method: 'post',
|
||||||
url: `https://nosup-hz1.127.net/yyimgs/${res.body.result.objectKey}?offset=0&complete=true&version=1.0`,
|
url: `https://nosup-hz1.127.net/yyimgs/${res.body.result.objectKey}?offset=0&complete=true&version=1.0`,
|
||||||
headers: {
|
headers: {
|
||||||
'x-nos-token': res.body.result.token,
|
'x-nos-token': res.body.result.token,
|
||||||
'Content-Type': 'image/jpeg',
|
'Content-Type': query.imgFile.mimetype || 'image/jpeg',
|
||||||
},
|
},
|
||||||
data: query.imgFile.data,
|
data: getUploadData(query.imgFile),
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// ...res.body.result,
|
|
||||||
// ...res2.data,
|
|
||||||
// ...res3.body,
|
|
||||||
url_pre: 'https://p1.music.126.net/' + res.body.result.objectKey,
|
url_pre: 'https://p1.music.126.net/' + res.body.result.objectKey,
|
||||||
imgId: res.body.result.docId,
|
imgId: res.body.result.docId,
|
||||||
}
|
}
|
||||||
|
|||||||
134
pnpm-lock.yaml
generated
134
pnpm-lock.yaml
generated
@ -9,29 +9,26 @@ importers:
|
|||||||
.:
|
.:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@neteasecloudmusicapienhanced/unblockmusic-utils':
|
'@neteasecloudmusicapienhanced/unblockmusic-utils':
|
||||||
specifier: ^0.1.3
|
specifier: ^0.2.2
|
||||||
version: 0.1.3
|
version: 0.2.2
|
||||||
axios:
|
axios:
|
||||||
specifier: ^1.13.4
|
specifier: ^1.13.5
|
||||||
version: 1.13.4
|
version: 1.13.5
|
||||||
crypto-js:
|
crypto-js:
|
||||||
specifier: ^4.2.0
|
specifier: ^4.2.0
|
||||||
version: 4.2.0
|
version: 4.2.0
|
||||||
dotenv:
|
dotenv:
|
||||||
specifier: ^17.2.3
|
specifier: ^17.2.4
|
||||||
version: 17.2.3
|
version: 17.2.4
|
||||||
express:
|
express:
|
||||||
specifier: ^5.2.1
|
specifier: ^5.2.1
|
||||||
version: 5.2.1
|
version: 5.2.1
|
||||||
express-fileupload:
|
express-fileupload:
|
||||||
specifier: ^1.5.2
|
specifier: ^1.5.2
|
||||||
version: 1.5.2
|
version: 1.5.2
|
||||||
md5:
|
|
||||||
specifier: ^2.3.0
|
|
||||||
version: 2.3.0
|
|
||||||
music-metadata:
|
music-metadata:
|
||||||
specifier: ^11.11.1
|
specifier: ^11.12.0
|
||||||
version: 11.11.1
|
version: 11.12.0
|
||||||
node-forge:
|
node-forge:
|
||||||
specifier: ^1.3.3
|
specifier: ^1.3.3
|
||||||
version: 1.3.3
|
version: 1.3.3
|
||||||
@ -73,37 +70,37 @@ importers:
|
|||||||
specifier: 25.0.9
|
specifier: 25.0.9
|
||||||
version: 25.0.9
|
version: 25.0.9
|
||||||
'@typescript-eslint/eslint-plugin':
|
'@typescript-eslint/eslint-plugin':
|
||||||
specifier: 8.46.3
|
specifier: ^8.46.3
|
||||||
version: 8.46.3(@typescript-eslint/parser@8.53.0(eslint@9.39.0)(typescript@5.9.3))(eslint@9.39.0)(typescript@5.9.3)
|
version: 8.46.3(@typescript-eslint/parser@8.53.0(eslint@9.39.0)(typescript@5.9.3))(eslint@9.39.0)(typescript@5.9.3)
|
||||||
'@typescript-eslint/parser':
|
'@typescript-eslint/parser':
|
||||||
specifier: 8.53.0
|
specifier: ^8.53.0
|
||||||
version: 8.53.0(eslint@9.39.0)(typescript@5.9.3)
|
version: 8.53.0(eslint@9.39.0)(typescript@5.9.3)
|
||||||
eslint:
|
eslint:
|
||||||
specifier: 9.39.0
|
specifier: ^9.39.0
|
||||||
version: 9.39.0
|
version: 9.39.0
|
||||||
eslint-config-prettier:
|
eslint-config-prettier:
|
||||||
specifier: 10.1.8
|
specifier: ^10.1.8
|
||||||
version: 10.1.8(eslint@9.39.0)
|
version: 10.1.8(eslint@9.39.0)
|
||||||
eslint-plugin-html:
|
eslint-plugin-html:
|
||||||
specifier: 8.1.3
|
specifier: ^8.1.3
|
||||||
version: 8.1.3
|
version: 8.1.3
|
||||||
eslint-plugin-prettier:
|
eslint-plugin-prettier:
|
||||||
specifier: 5.5.5
|
specifier: ^5.5.5
|
||||||
version: 5.5.5(eslint-config-prettier@10.1.8(eslint@9.39.0))(eslint@9.39.0)(prettier@3.8.1)
|
version: 5.5.5(eslint-config-prettier@10.1.8(eslint@9.39.0))(eslint@9.39.0)(prettier@3.8.1)
|
||||||
globals:
|
globals:
|
||||||
specifier: ^16.5.0
|
specifier: ^16.5.0
|
||||||
version: 16.5.0
|
version: 16.5.0
|
||||||
husky:
|
husky:
|
||||||
specifier: 9.1.7
|
specifier: ^9.1.7
|
||||||
version: 9.1.7
|
version: 9.1.7
|
||||||
intelli-espower-loader:
|
intelli-espower-loader:
|
||||||
specifier: 1.1.0
|
specifier: ^1.1.0
|
||||||
version: 1.1.0
|
version: 1.1.0
|
||||||
lint-staged:
|
lint-staged:
|
||||||
specifier: 16.2.7
|
specifier: ^16.2.7
|
||||||
version: 16.2.7
|
version: 16.2.7
|
||||||
mocha:
|
mocha:
|
||||||
specifier: 11.7.5
|
specifier: ^11.7.5
|
||||||
version: 11.7.5
|
version: 11.7.5
|
||||||
nodemon:
|
nodemon:
|
||||||
specifier: ^3.1.11
|
specifier: ^3.1.11
|
||||||
@ -112,13 +109,13 @@ importers:
|
|||||||
specifier: ^5.8.1
|
specifier: ^5.8.1
|
||||||
version: 5.8.1
|
version: 5.8.1
|
||||||
power-assert:
|
power-assert:
|
||||||
specifier: 1.6.1
|
specifier: ^1.6.1
|
||||||
version: 1.6.1
|
version: 1.6.1
|
||||||
prettier:
|
prettier:
|
||||||
specifier: 3.8.1
|
specifier: ^3.7.4
|
||||||
version: 3.8.1
|
version: 3.8.1
|
||||||
typescript:
|
typescript:
|
||||||
specifier: 5.9.3
|
specifier: ^5.9.3
|
||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
@ -222,8 +219,8 @@ packages:
|
|||||||
'@jridgewell/trace-mapping@0.3.31':
|
'@jridgewell/trace-mapping@0.3.31':
|
||||||
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
|
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
|
||||||
|
|
||||||
'@neteasecloudmusicapienhanced/unblockmusic-utils@0.1.3':
|
'@neteasecloudmusicapienhanced/unblockmusic-utils@0.2.2':
|
||||||
resolution: {integrity: sha512-X6DtL26AtRw4SsAG5iW+0ZbcvCrdhXLlaCSL38UWp1HW+F5+QXocBtzE6wkcZN/ZXAiKpMow2+Y1Igjx/foN/A==}
|
resolution: {integrity: sha512-3YdfPWL/bLhsdUtvZ4nBEgeZjyy0QLD7Py9v7uwEt7V2WQK+q1qV2EWKn9TkCkeOujwCMNvGbhiWouk0PJnP6g==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
'@nodelib/fs.scandir@2.1.5':
|
'@nodelib/fs.scandir@2.1.5':
|
||||||
@ -436,8 +433,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-S2Hw0TtNkMJhIabBwIojKL9YHO5T0n5eNqWJ7Lrlel/zDbftQpxpapi8tZs3X1HWa+u+QeydGmzzNU0m09+Rcg==}
|
resolution: {integrity: sha512-S2Hw0TtNkMJhIabBwIojKL9YHO5T0n5eNqWJ7Lrlel/zDbftQpxpapi8tZs3X1HWa+u+QeydGmzzNU0m09+Rcg==}
|
||||||
engines: {node: '>=0.4.2'}
|
engines: {node: '>=0.4.2'}
|
||||||
|
|
||||||
ansi-escapes@7.2.0:
|
ansi-escapes@7.3.0:
|
||||||
resolution: {integrity: sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw==}
|
resolution: {integrity: sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
ansi-regex@5.0.1:
|
ansi-regex@5.0.1:
|
||||||
@ -515,8 +512,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
|
resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
axios@1.13.4:
|
axios@1.13.5:
|
||||||
resolution: {integrity: sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg==}
|
resolution: {integrity: sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==}
|
||||||
|
|
||||||
balanced-match@1.0.2:
|
balanced-match@1.0.2:
|
||||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||||
@ -610,9 +607,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
charenc@0.0.2:
|
|
||||||
resolution: {integrity: sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==}
|
|
||||||
|
|
||||||
chokidar@3.6.0:
|
chokidar@3.6.0:
|
||||||
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
|
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
|
||||||
engines: {node: '>= 8.10.0'}
|
engines: {node: '>= 8.10.0'}
|
||||||
@ -666,8 +660,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
|
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
commander@14.0.2:
|
commander@14.0.3:
|
||||||
resolution: {integrity: sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==}
|
resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==}
|
||||||
engines: {node: '>=20'}
|
engines: {node: '>=20'}
|
||||||
|
|
||||||
concat-map@0.0.1:
|
concat-map@0.0.1:
|
||||||
@ -710,9 +704,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
|
|
||||||
crypt@0.0.2:
|
|
||||||
resolution: {integrity: sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==}
|
|
||||||
|
|
||||||
crypto-js@4.2.0:
|
crypto-js@4.2.0:
|
||||||
resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==}
|
resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==}
|
||||||
|
|
||||||
@ -834,8 +825,8 @@ packages:
|
|||||||
domutils@3.2.2:
|
domutils@3.2.2:
|
||||||
resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==}
|
resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==}
|
||||||
|
|
||||||
dotenv@17.2.3:
|
dotenv@17.2.4:
|
||||||
resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==}
|
resolution: {integrity: sha512-mudtfb4zRB4bVvdj0xRo+e6duH1csJRM8IukBqfTRvHotn9+LBXB8ynAidP9zHqoRC/fsllXgk4kCKlR21fIhw==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
dunder-proto@1.0.1:
|
dunder-proto@1.0.1:
|
||||||
@ -1271,6 +1262,7 @@ packages:
|
|||||||
|
|
||||||
glob@10.5.0:
|
glob@10.5.0:
|
||||||
resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==}
|
resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==}
|
||||||
|
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
globals@14.0.0:
|
globals@14.0.0:
|
||||||
@ -1447,9 +1439,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==}
|
resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
is-buffer@1.1.6:
|
|
||||||
resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==}
|
|
||||||
|
|
||||||
is-callable@1.2.7:
|
is-callable@1.2.7:
|
||||||
resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==}
|
resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@ -1650,9 +1639,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
md5@2.3.0:
|
|
||||||
resolution: {integrity: sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==}
|
|
||||||
|
|
||||||
media-typer@0.3.0:
|
media-typer@0.3.0:
|
||||||
resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
|
resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
@ -1750,8 +1736,8 @@ packages:
|
|||||||
multistream@4.1.0:
|
multistream@4.1.0:
|
||||||
resolution: {integrity: sha512-J1XDiAmmNpRCBfIWJv+n0ymC4ABcf/Pl+5YvC5B/D2f/2+8PtHvCNxMPKiQcZyi922Hq69J2YOpb1pTywfifyw==}
|
resolution: {integrity: sha512-J1XDiAmmNpRCBfIWJv+n0ymC4ABcf/Pl+5YvC5B/D2f/2+8PtHvCNxMPKiQcZyi922Hq69J2YOpb1pTywfifyw==}
|
||||||
|
|
||||||
music-metadata@11.11.1:
|
music-metadata@11.12.0:
|
||||||
resolution: {integrity: sha512-8FT+lSLznASDhn5KNJtQE6ZH95VqhxtKWNPrvdfhlqgbdZZEEAXehx+xpUvas4VuEZAu49BhQgLa3NlmPeRaww==}
|
resolution: {integrity: sha512-9ChYnmVmyHvFxR2g0MWFSHmJfbssRy07457G4gbb4LA9WYvyZea/8EMbqvg5dcv4oXNCNL01m8HXtymLlhhkYg==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
nano-spawn@2.0.0:
|
nano-spawn@2.0.0:
|
||||||
@ -2177,8 +2163,8 @@ packages:
|
|||||||
secure-json-parse@2.7.0:
|
secure-json-parse@2.7.0:
|
||||||
resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==}
|
resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==}
|
||||||
|
|
||||||
semver@7.7.3:
|
semver@7.7.4:
|
||||||
resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==}
|
resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
@ -2773,10 +2759,10 @@ snapshots:
|
|||||||
'@jridgewell/resolve-uri': 3.1.2
|
'@jridgewell/resolve-uri': 3.1.2
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
|
|
||||||
'@neteasecloudmusicapienhanced/unblockmusic-utils@0.1.3':
|
'@neteasecloudmusicapienhanced/unblockmusic-utils@0.2.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@unblockneteasemusic/server': 0.28.0
|
'@unblockneteasemusic/server': 0.28.0
|
||||||
axios: 1.13.4
|
axios: 1.13.5
|
||||||
express: 4.22.1
|
express: 4.22.1
|
||||||
https: 1.0.0
|
https: 1.0.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@ -2898,8 +2884,8 @@ snapshots:
|
|||||||
|
|
||||||
'@typescript-eslint/project-service@8.46.3(typescript@5.9.3)':
|
'@typescript-eslint/project-service@8.46.3(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/tsconfig-utils': 8.46.3(typescript@5.9.3)
|
'@typescript-eslint/tsconfig-utils': 8.53.0(typescript@5.9.3)
|
||||||
'@typescript-eslint/types': 8.46.3
|
'@typescript-eslint/types': 8.53.0
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@ -2958,7 +2944,7 @@ snapshots:
|
|||||||
fast-glob: 3.3.3
|
fast-glob: 3.3.3
|
||||||
is-glob: 4.0.3
|
is-glob: 4.0.3
|
||||||
minimatch: 9.0.5
|
minimatch: 9.0.5
|
||||||
semver: 7.7.3
|
semver: 7.7.4
|
||||||
ts-api-utils: 2.4.0(typescript@5.9.3)
|
ts-api-utils: 2.4.0(typescript@5.9.3)
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@ -2972,7 +2958,7 @@ snapshots:
|
|||||||
'@typescript-eslint/visitor-keys': 8.53.0
|
'@typescript-eslint/visitor-keys': 8.53.0
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
minimatch: 9.0.5
|
minimatch: 9.0.5
|
||||||
semver: 7.7.3
|
semver: 7.7.4
|
||||||
tinyglobby: 0.2.15
|
tinyglobby: 0.2.15
|
||||||
ts-api-utils: 2.4.0(typescript@5.9.3)
|
ts-api-utils: 2.4.0(typescript@5.9.3)
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
@ -3043,7 +3029,7 @@ snapshots:
|
|||||||
|
|
||||||
amdefine@1.0.1: {}
|
amdefine@1.0.1: {}
|
||||||
|
|
||||||
ansi-escapes@7.2.0:
|
ansi-escapes@7.3.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
environment: 1.1.0
|
environment: 1.1.0
|
||||||
|
|
||||||
@ -3114,7 +3100,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
possible-typed-array-names: 1.1.0
|
possible-typed-array-names: 1.1.0
|
||||||
|
|
||||||
axios@1.13.4:
|
axios@1.13.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
follow-redirects: 1.15.11
|
follow-redirects: 1.15.11
|
||||||
form-data: 4.0.5
|
form-data: 4.0.5
|
||||||
@ -3238,8 +3224,6 @@ snapshots:
|
|||||||
ansi-styles: 4.3.0
|
ansi-styles: 4.3.0
|
||||||
supports-color: 7.2.0
|
supports-color: 7.2.0
|
||||||
|
|
||||||
charenc@0.0.2: {}
|
|
||||||
|
|
||||||
chokidar@3.6.0:
|
chokidar@3.6.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
anymatch: 3.1.3
|
anymatch: 3.1.3
|
||||||
@ -3309,7 +3293,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
delayed-stream: 1.0.0
|
delayed-stream: 1.0.0
|
||||||
|
|
||||||
commander@14.0.2: {}
|
commander@14.0.3: {}
|
||||||
|
|
||||||
concat-map@0.0.1: {}
|
concat-map@0.0.1: {}
|
||||||
|
|
||||||
@ -3339,8 +3323,6 @@ snapshots:
|
|||||||
shebang-command: 2.0.0
|
shebang-command: 2.0.0
|
||||||
which: 2.0.2
|
which: 2.0.2
|
||||||
|
|
||||||
crypt@0.0.2: {}
|
|
||||||
|
|
||||||
crypto-js@4.2.0: {}
|
crypto-js@4.2.0: {}
|
||||||
|
|
||||||
d@1.0.2:
|
d@1.0.2:
|
||||||
@ -3465,7 +3447,7 @@ snapshots:
|
|||||||
domelementtype: 2.3.0
|
domelementtype: 2.3.0
|
||||||
domhandler: 5.0.3
|
domhandler: 5.0.3
|
||||||
|
|
||||||
dotenv@17.2.3: {}
|
dotenv@17.2.4: {}
|
||||||
|
|
||||||
dunder-proto@1.0.1:
|
dunder-proto@1.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -4276,8 +4258,6 @@ snapshots:
|
|||||||
call-bound: 1.0.4
|
call-bound: 1.0.4
|
||||||
has-tostringtag: 1.0.2
|
has-tostringtag: 1.0.2
|
||||||
|
|
||||||
is-buffer@1.1.6: {}
|
|
||||||
|
|
||||||
is-callable@1.2.7: {}
|
is-callable@1.2.7: {}
|
||||||
|
|
||||||
is-core-module@2.16.1:
|
is-core-module@2.16.1:
|
||||||
@ -4433,7 +4413,7 @@ snapshots:
|
|||||||
|
|
||||||
lint-staged@16.2.7:
|
lint-staged@16.2.7:
|
||||||
dependencies:
|
dependencies:
|
||||||
commander: 14.0.2
|
commander: 14.0.3
|
||||||
listr2: 9.0.5
|
listr2: 9.0.5
|
||||||
micromatch: 4.0.8
|
micromatch: 4.0.8
|
||||||
nano-spawn: 2.0.0
|
nano-spawn: 2.0.0
|
||||||
@ -4467,7 +4447,7 @@ snapshots:
|
|||||||
|
|
||||||
log-update@6.1.0:
|
log-update@6.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
ansi-escapes: 7.2.0
|
ansi-escapes: 7.3.0
|
||||||
cli-cursor: 5.0.0
|
cli-cursor: 5.0.0
|
||||||
slice-ansi: 7.1.2
|
slice-ansi: 7.1.2
|
||||||
strip-ansi: 7.1.2
|
strip-ansi: 7.1.2
|
||||||
@ -4477,12 +4457,6 @@ snapshots:
|
|||||||
|
|
||||||
math-intrinsics@1.1.0: {}
|
math-intrinsics@1.1.0: {}
|
||||||
|
|
||||||
md5@2.3.0:
|
|
||||||
dependencies:
|
|
||||||
charenc: 0.0.2
|
|
||||||
crypt: 0.0.2
|
|
||||||
is-buffer: 1.1.6
|
|
||||||
|
|
||||||
media-typer@0.3.0: {}
|
media-typer@0.3.0: {}
|
||||||
|
|
||||||
media-typer@1.1.0: {}
|
media-typer@1.1.0: {}
|
||||||
@ -4575,7 +4549,7 @@ snapshots:
|
|||||||
once: 1.4.0
|
once: 1.4.0
|
||||||
readable-stream: 3.6.2
|
readable-stream: 3.6.2
|
||||||
|
|
||||||
music-metadata@11.11.1:
|
music-metadata@11.12.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@borewit/text-codec': 0.2.1
|
'@borewit/text-codec': 0.2.1
|
||||||
'@tokenizer/token': 0.3.0
|
'@tokenizer/token': 0.3.0
|
||||||
@ -4606,7 +4580,7 @@ snapshots:
|
|||||||
|
|
||||||
node-abi@3.87.0:
|
node-abi@3.87.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
semver: 7.7.3
|
semver: 7.7.4
|
||||||
|
|
||||||
node-fetch@2.7.0:
|
node-fetch@2.7.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -4626,7 +4600,7 @@ snapshots:
|
|||||||
ignore-by-default: 1.0.1
|
ignore-by-default: 1.0.1
|
||||||
minimatch: 3.1.2
|
minimatch: 3.1.2
|
||||||
pstree.remy: 1.1.8
|
pstree.remy: 1.1.8
|
||||||
semver: 7.7.3
|
semver: 7.7.4
|
||||||
simple-update-notifier: 2.0.0
|
simple-update-notifier: 2.0.0
|
||||||
supports-color: 5.5.0
|
supports-color: 5.5.0
|
||||||
touch: 3.1.1
|
touch: 3.1.1
|
||||||
@ -4803,7 +4777,7 @@ snapshots:
|
|||||||
https-proxy-agent: 5.0.1
|
https-proxy-agent: 5.0.1
|
||||||
node-fetch: 2.7.0
|
node-fetch: 2.7.0
|
||||||
progress: 2.0.3
|
progress: 2.0.3
|
||||||
semver: 7.7.3
|
semver: 7.7.4
|
||||||
tar-fs: 2.1.4
|
tar-fs: 2.1.4
|
||||||
yargs: 16.2.0
|
yargs: 16.2.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@ -5096,7 +5070,7 @@ snapshots:
|
|||||||
|
|
||||||
secure-json-parse@2.7.0: {}
|
secure-json-parse@2.7.0: {}
|
||||||
|
|
||||||
semver@7.7.3: {}
|
semver@7.7.4: {}
|
||||||
|
|
||||||
send@0.19.2:
|
send@0.19.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -5226,7 +5200,7 @@ snapshots:
|
|||||||
|
|
||||||
simple-update-notifier@2.0.0:
|
simple-update-notifier@2.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
semver: 7.7.3
|
semver: 7.7.4
|
||||||
|
|
||||||
slash@3.0.0: {}
|
slash@3.0.0: {}
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
128
public/api.html
128
public/api.html
@ -5,66 +5,128 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>API 调试界面</title>
|
<title>API 调试界面</title>
|
||||||
<style>
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: Arial, sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
margin: 20px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
|
background: #f5f5f5;
|
||||||
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
display: flex;
|
max-width: 1200px;
|
||||||
flex-direction: column;
|
margin: 0 auto;
|
||||||
flex-grow: 1;
|
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 {
|
form {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
}
|
||||||
input, button {
|
|
||||||
padding: 10px;
|
label {
|
||||||
box-sizing: border-box;
|
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;
|
flex: 1;
|
||||||
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input:focus, select:focus {
|
||||||
|
border-color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
background-color: #4CAF50;
|
background: #333;
|
||||||
color: white;
|
color: white;
|
||||||
|
padding: 10px 24px;
|
||||||
border: none;
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
transition: background 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background: #555;
|
||||||
|
}
|
||||||
|
|
||||||
.data-result {
|
.data-result {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
gap: 16px;
|
||||||
flex-grow: 1;
|
min-height: 400px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.data-result > div {
|
.data-result > div {
|
||||||
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex-grow: 1;
|
|
||||||
padding: 10px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.data-result label {
|
.data-result label {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 8px;
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
#data, #result {
|
|
||||||
height: 100%;
|
textarea {
|
||||||
box-sizing: border-box;
|
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 {
|
|
||||||
border-right: 1px solid #ccc;
|
textarea:focus {
|
||||||
|
border-color: #333;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
<h1>API 调试界面</h1>
|
||||||
<form onsubmit="event.preventDefault(); sendRequest();">
|
<form onsubmit="event.preventDefault(); sendRequest();">
|
||||||
<label for="uri">uri</label>
|
<div class="form-row">
|
||||||
|
<label for="uri">URI</label>
|
||||||
<input type="text" id="uri" name="uri" value="/api/song/lyric/v1">
|
<input type="text" id="uri" name="uri" value="/api/song/lyric/v1">
|
||||||
<label for="crypto">crypto</label>
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="crypto">加密方式</label>
|
||||||
<select id="crypto" name="crypto">
|
<select id="crypto" name="crypto">
|
||||||
<option value="weapi">weapi</option>
|
<option value="weapi">weapi</option>
|
||||||
<option value="eapi">eapi</option>
|
<option value="eapi">eapi</option>
|
||||||
@ -72,15 +134,19 @@
|
|||||||
<option value="linuxapi">linuxapi</option>
|
<option value="linuxapi">linuxapi</option>
|
||||||
<option value="" selected>(默认)</option>
|
<option value="" selected>(默认)</option>
|
||||||
</select>
|
</select>
|
||||||
<button type="submit">发送</button>
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<label></label>
|
||||||
|
<button type="submit">发送请求</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<div class="data-result">
|
<div class="data-result">
|
||||||
<div>
|
<div>
|
||||||
<label for="result">result</label>
|
<label for="result">响应结果</label>
|
||||||
<textarea id="result" name="result"></textarea>
|
<textarea id="result" name="result" readonly></textarea>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="data">data</label>
|
<label for="data">请求数据</label>
|
||||||
<textarea id="data" name="data">
|
<textarea id="data" name="data">
|
||||||
{
|
{
|
||||||
"cp": false,
|
"cp": false,
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
'use strict'
|
'use strict'
|
||||||
const WASM_BINARY_PLACEHOLDER = 'WASM_BINARY_PLACEHOLDER';
|
const WASM_BINARY_PLACEHOLDER = 'WASM_BINARY_PLACEHOLDER';
|
||||||
const logger = require('../../util/logger.js')
|
|
||||||
// See https://github.com/Distributive-Network/PythonMonkey/issues/266
|
// See https://github.com/Distributive-Network/PythonMonkey/issues/266
|
||||||
if (typeof globalThis.setInterval != 'function'){
|
if (typeof globalThis.setInterval != 'function'){
|
||||||
globalThis.setInterval = function pm$$setInterval(fn, timeout) {
|
globalThis.setInterval = function pm$$setInterval(fn, timeout) {
|
||||||
@ -1612,9 +1611,9 @@ function instantiateRuntime(){
|
|||||||
|
|
||||||
function GenerateFP(floatArray) {
|
function GenerateFP(floatArray) {
|
||||||
let PCMBuffer = Float32Array.from(floatArray)
|
let PCMBuffer = Float32Array.from(floatArray)
|
||||||
logger.info('[afp] input samples n=', PCMBuffer.length)
|
console.info('[afp] input samples n=', PCMBuffer.length)
|
||||||
return instantiateRuntime().then((fpRuntime) => {
|
return instantiateRuntime().then((fpRuntime) => {
|
||||||
logger.info('[afp] begin fingerprinting')
|
console.info('[afp] begin fingerprinting')
|
||||||
let fp_vector = fpRuntime.ExtractQueryFP(PCMBuffer.buffer)
|
let fp_vector = fpRuntime.ExtractQueryFP(PCMBuffer.buffer)
|
||||||
let result_buf = new Uint8Array(fp_vector.size());
|
let result_buf = new Uint8Array(fp_vector.size());
|
||||||
for (let t = 0; t < fp_vector.size(); t++)
|
for (let t = 0; t < fp_vector.size(); t++)
|
||||||
|
|||||||
@ -1,27 +1,142 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>听歌识曲 Demo</title>
|
||||||
<style>
|
<style>
|
||||||
* {
|
* {
|
||||||
font-family: sans-serif;
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
pre {
|
body {
|
||||||
font-family: monospace;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
font-family: sans-serif;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
audio {
|
audio {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
canvas {
|
canvas {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 0;
|
height: 0;
|
||||||
transition: all linear 0.1s;
|
transition: all linear 0.1s;
|
||||||
|
background: #f9f9f9;
|
||||||
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.canvas-active {
|
.canvas-active {
|
||||||
@ -29,39 +144,80 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
pre {
|
pre {
|
||||||
overflow: scroll;
|
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;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<h1>听歌识曲 Demo (Credit: <a href="https://github.com/mos9527/ncm-afp" target="_blank">https://github.com/mos9527/ncm-afp</a>)</h1>
|
<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>
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
<p><b>DISCLAIMER: </b></p>
|
|
||||||
<p>This site uses the offical NetEase audio matcher APIs (reverse engineered from <a
|
<div class="warning">
|
||||||
href="https://fn.music.163.com/g/chrome-extension-home-page-beta/">https://fn.music.163.com/g/chrome-extension-home-page-beta/</a>)
|
<strong>免责声明:</strong>本站点使用网易云音乐官方音频识别API(逆向自 <a href="https://fn.music.163.com/g/chrome-extension-home-page-beta/" target="_blank">Chrome 扩展页面</a>),不鼓励版权侵犯或知识产权盗窃。
|
||||||
</p>
|
</div>
|
||||||
<p>And DOES NOT condone copyright infringment nor intellectual property theft.</p>
|
|
||||||
|
<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>
|
<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>
|
|
||||||
|
|
||||||
<audio id="audio" controls autoplay></audio>
|
<audio id="audio" controls autoplay></audio>
|
||||||
<canvas id="canvas"></canvas>
|
<canvas id="canvas"></canvas>
|
||||||
<button id="invoke">Clip</button>
|
|
||||||
|
<div class="control-group">
|
||||||
|
<button id="invoke">识别</button>
|
||||||
<input type="file" name="picker" accept="*" id="file">
|
<input type="file" name="picker" accept="*" id="file">
|
||||||
<hr>
|
</div>
|
||||||
<label for="use-mic">Mix in Microphone input</label>
|
|
||||||
|
<div class="control-group">
|
||||||
|
<div class="checkbox-group">
|
||||||
<input type="checkbox" name="use-mic" id="usemic">
|
<input type="checkbox" name="use-mic" id="usemic">
|
||||||
|
<label for="usemic">混合麦克风输入</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
|
<h3 style="font-size: 16px; font-weight: 600; color: #333; margin-bottom: 12px;">日志</h3>
|
||||||
<pre id="logs"></pre>
|
<pre id="logs"></pre>
|
||||||
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
<script src="./afp.wasm.js"></script>
|
<script src="./afp.wasm.js"></script>
|
||||||
<script src="./afp.js"></script>
|
<script src="./afp.js"></script>
|
||||||
<script type="module">
|
<script type="module">
|
||||||
@ -76,13 +232,17 @@
|
|||||||
let canvas = document.getElementById('canvas')
|
let canvas = document.getElementById('canvas')
|
||||||
let canvasCtx = canvas.getContext('2d')
|
let canvasCtx = canvas.getContext('2d')
|
||||||
let logs = document.getElementById('logs')
|
let logs = document.getElementById('logs')
|
||||||
logs.write = line => logs.innerHTML += line + '\n'
|
logs.write = line => {
|
||||||
|
// Append log lines as text to avoid interpreting content as HTML
|
||||||
|
logs.appendChild(document.createTextNode(line));
|
||||||
|
logs.appendChild(document.createElement('br'));
|
||||||
|
}
|
||||||
|
|
||||||
function RecorderCallback(channelL) {
|
function RecorderCallback(channelL) {
|
||||||
let sampleBuffer = new Float32Array(channelL.subarray(0, duration * 8000))
|
let sampleBuffer = new Float32Array(channelL.subarray(0, duration * 8000))
|
||||||
GenerateFP(sampleBuffer).then(FP => {
|
GenerateFP(sampleBuffer).then(FP => {
|
||||||
logs.write(`[index] Generated FP ${FP}`)
|
logs.write(`[index] 生成指纹 ${FP}`)
|
||||||
logs.write('[index] Now querying, please wait...')
|
logs.write('[index] 正在查询,请稍候...')
|
||||||
fetch(
|
fetch(
|
||||||
'/audio/match?' +
|
'/audio/match?' +
|
||||||
new URLSearchParams({
|
new URLSearchParams({
|
||||||
@ -91,9 +251,9 @@
|
|||||||
method: 'POST'
|
method: 'POST'
|
||||||
}).then(resp => resp.json()).then(resp => {
|
}).then(resp => resp.json()).then(resp => {
|
||||||
if (!resp.data.result) {
|
if (!resp.data.result) {
|
||||||
return logs.write('[index] Query failed with no results.')
|
return logs.write('[index] 查询失败,无结果')
|
||||||
}
|
}
|
||||||
logs.write(`[index] Query complete. Results=${resp.data.result.length}`)
|
logs.write(`[index] 查询完成。结果数量=${resp.data.result.length}`)
|
||||||
for (var song of resp.data.result) {
|
for (var song of resp.data.result) {
|
||||||
logs.write(
|
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>`
|
`[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>`
|
||||||
@ -104,20 +264,19 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function InitAudioCtx() {
|
function InitAudioCtx() {
|
||||||
// AFP.wasm can't do it with anything other than 8KHz
|
|
||||||
audioCtx = new AudioContext({ 'sampleRate': 8000 })
|
audioCtx = new AudioContext({ 'sampleRate': 8000 })
|
||||||
if (audioCtx.state == 'suspended')
|
if (audioCtx.state == 'suspended')
|
||||||
return false
|
return false
|
||||||
let audioNode = audioCtx.createMediaElementSource(audio)
|
let audioNode = audioCtx.createMediaElementSource(audio)
|
||||||
audioCtx.audioWorklet.addModule('rec.js').then(() => {
|
audioCtx.audioWorklet.addModule('rec.js').then(() => {
|
||||||
recorderNode = new AudioWorkletNode(audioCtx, 'timed-recorder')
|
recorderNode = new AudioWorkletNode(audioCtx, 'timed-recorder')
|
||||||
audioNode.connect(recorderNode) // recorderNode doesn't output anything
|
audioNode.connect(recorderNode)
|
||||||
audioNode.connect(audioCtx.destination)
|
audioNode.connect(audioCtx.destination)
|
||||||
recorderNode.port.onmessage = event => {
|
recorderNode.port.onmessage = event => {
|
||||||
switch (event.data.message) {
|
switch (event.data.message) {
|
||||||
case 'finished':
|
case 'finished':
|
||||||
RecorderCallback(event.data.recording)
|
RecorderCallback(event.data.recording)
|
||||||
clip.innerHTML = 'Clip'
|
clip.innerHTML = '识别'
|
||||||
clip.disabled = false
|
clip.disabled = false
|
||||||
canvas.classList.remove('canvas-active')
|
canvas.classList.remove('canvas-active')
|
||||||
break
|
break
|
||||||
@ -130,7 +289,6 @@
|
|||||||
logs.write(event.data.message)
|
logs.write(event.data.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Attempt to get user's microphone and connect it to the AudioContext.
|
|
||||||
navigator.mediaDevices.getUserMedia({
|
navigator.mediaDevices.getUserMedia({
|
||||||
audio: {
|
audio: {
|
||||||
echoCancellation: false,
|
echoCancellation: false,
|
||||||
@ -142,7 +300,7 @@
|
|||||||
micSourceNode = audioCtx.createMediaStreamSource(micStream);
|
micSourceNode = audioCtx.createMediaStreamSource(micStream);
|
||||||
micSourceNode.connect(recorderNode)
|
micSourceNode.connect(recorderNode)
|
||||||
usemic.checked = true
|
usemic.checked = true
|
||||||
logs.write('[rec.js] Microphone attached.')
|
logs.write('[rec.js] 麦克风已连接')
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return true
|
return true
|
||||||
@ -161,10 +319,20 @@
|
|||||||
else
|
else
|
||||||
micSourceNode.connect(recorderNode)
|
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.addEventListener('change', event => {
|
||||||
file.files[0].arrayBuffer().then(
|
file.files[0].arrayBuffer().then(
|
||||||
async buffer => {
|
async buffer => {
|
||||||
logs.write(`[index] File ${file.files[0].name} loaded.`)
|
const safeName = escapeHtml(file.files[0].name)
|
||||||
|
logs.write(`[index] 文件 ${safeName} 已加载`)
|
||||||
audio.src = window.URL.createObjectURL(new Blob([buffer]))
|
audio.src = window.URL.createObjectURL(new Blob([buffer]))
|
||||||
clip.disabled = false
|
clip.disabled = false
|
||||||
})
|
})
|
||||||
@ -188,12 +356,13 @@
|
|||||||
UpdateCanvas()
|
UpdateCanvas()
|
||||||
let requestCtx = setInterval(() => {
|
let requestCtx = setInterval(() => {
|
||||||
try {
|
try {
|
||||||
if (InitAudioCtx()) { // Put this here so we don't have to deal with the 'user did not interact' thing
|
if (InitAudioCtx()) {
|
||||||
clearInterval(requestCtx)
|
clearInterval(requestCtx)
|
||||||
logs.write('[rec.js] Audio Context started.')
|
logs.write('[rec.js] 音频上下文已启动')
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Fail silently
|
// Fail silently
|
||||||
}
|
}
|
||||||
}, 100)
|
}, 100)
|
||||||
</script>
|
</script>
|
||||||
|
</html>
|
||||||
@ -5,67 +5,320 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>更新头像</title>
|
<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>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div>
|
<div class="container">
|
||||||
<a href="/qrlogin-nocookie.html">
|
<h1>更新头像</h1>
|
||||||
如果没登录,请先登录
|
<p class="subtitle">选择一张图片作为您的新头像</p>
|
||||||
</a>
|
|
||||||
|
<div class="avatar-wrapper" id="avatarWrapper">
|
||||||
|
<img id="avatar" class="avatar" src="" alt="头像" />
|
||||||
|
<div class="loading-overlay">
|
||||||
|
<div class="spinner"></div>
|
||||||
</div>
|
</div>
|
||||||
<input id="file" type="file" />
|
</div>
|
||||||
<img id="avatar" style="height: 200px; width: 200px; border-radius: 50%" />
|
|
||||||
<script src="https://fastly.jsdelivr.net/npm/axios@0.26.1/dist/axios.min.js
|
<label class="upload-btn">
|
||||||
"></script>
|
选择图片
|
||||||
|
<input id="file" type="file" accept="image/*" />
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<a href="/qrlogin-nocookie.html" class="login-link">
|
||||||
|
还没有登录?点击登录
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<p class="hint">支持 JPG、PNG 格式,建议尺寸 200x200</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="toast" class="toast"></div>
|
||||||
|
|
||||||
|
<script src="https://fastly.jsdelivr.net/npm/axios@0.26.1/dist/axios.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
main()
|
main()
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
document.querySelector('input[type="file"]').addEventListener(
|
const fileInput = document.querySelector('input[type="file"]');
|
||||||
'change',
|
const avatarWrapper = document.getElementById('avatarWrapper');
|
||||||
function (e) {
|
|
||||||
var file = this.files[0]
|
fileInput.addEventListener('change', function(e) {
|
||||||
upload(file)
|
const file = this.files[0];
|
||||||
},
|
if (file) {
|
||||||
false,
|
upload(file);
|
||||||
)
|
}
|
||||||
|
}, false);
|
||||||
|
|
||||||
|
try {
|
||||||
|
showToast('正在加载头像...', 'info');
|
||||||
|
avatarWrapper.classList.add('loading');
|
||||||
const res = await axios({
|
const res = await axios({
|
||||||
url: `/user/detail?uid=32953014×tamp=${Date.now()}`,
|
url: `/user/detail?uid=32953014×tamp=${Date.now()}`,
|
||||||
withCredentials: true, //跨域的话必须设置
|
withCredentials: true
|
||||||
})
|
});
|
||||||
document.querySelector('#avatar').src = res.data.profile.avatarUrl
|
document.querySelector('#avatar').src = res.data.profile.avatarUrl;
|
||||||
|
hideToast();
|
||||||
|
} catch (error) {
|
||||||
|
hideToast();
|
||||||
|
showToast('加载头像失败,请刷新页面重试', 'error');
|
||||||
|
console.error('加载头像失败:', error);
|
||||||
|
} finally {
|
||||||
|
avatarWrapper.classList.remove('loading');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function upload(file) {
|
async function upload(file) {
|
||||||
var formData = new FormData()
|
const avatarWrapper = document.getElementById('avatarWrapper');
|
||||||
formData.append('imgFile', file)
|
|
||||||
const imgSize = await getImgSize(file)
|
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({
|
const res = await axios({
|
||||||
method: 'post',
|
method: 'post',
|
||||||
url: `/avatar/upload?cookie=${localStorage.getItem('cookie')}&imgSize=${imgSize.width
|
url: `/avatar/upload?cookie=${localStorage.getItem('cookie')}&imgSize=${imgSize.width}&imgX=0&imgY=0×tamp=${Date.now()}`,
|
||||||
}&imgX=0&imgY=0×tamp=${Date.now()}`,
|
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'multipart/form-data',
|
'Content-Type': 'multipart/form-data',
|
||||||
},
|
},
|
||||||
data: formData,
|
data: formData,
|
||||||
})
|
});
|
||||||
document.querySelector('#avatar').src = res.data.data.url
|
|
||||||
|
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');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getImgSize(file) {
|
function getImgSize(file) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let reader = new FileReader()
|
let reader = new FileReader();
|
||||||
reader.readAsDataURL(file)
|
reader.readAsDataURL(file);
|
||||||
reader.onload = function(theFile) {
|
reader.onload = function(theFile) {
|
||||||
let image = new Image()
|
let image = new Image();
|
||||||
image.src = theFile.target.result
|
image.src = theFile.target.result;
|
||||||
image.onload = function() {
|
image.onload = function() {
|
||||||
resolve({
|
resolve({
|
||||||
width: this.width,
|
width: this.width,
|
||||||
height: this.height,
|
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>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@ -4,26 +4,285 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>云盘上传</title>
|
<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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div>
|
<div class="container">
|
||||||
<a href="/qrlogin-nocookie.html"> 如果没登录,请先登录 </a>
|
<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>
|
||||||
<input id="file" type="file" multiple />
|
</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 id="app">
|
||||||
<ul>
|
<div v-if="loading" class="loading">加载中...</div>
|
||||||
<li v-for="(item,index) in songs" :key="index">{{item.songName}}</li>
|
<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>
|
</ul>
|
||||||
|
<div v-else class="empty-state">暂无云盘歌曲</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="https://fastly.jsdelivr.net/npm/axios@0.26.1/dist/axios.min.js"></script>
|
<script src="https://fastly.jsdelivr.net/npm/axios@0.26.1/dist/axios.min.js"></script>
|
||||||
<script src="https://fastly.jsdelivr.net/npm/vue"></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>
|
<script>
|
||||||
const app = Vue.createApp({
|
const app = Vue.createApp({
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
songs: [],
|
songs: [],
|
||||||
|
loading: false,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
@ -31,73 +290,289 @@
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
getData() {
|
getData() {
|
||||||
console.info('getdata')
|
this.loading = true
|
||||||
const _this = this
|
|
||||||
axios({
|
axios({
|
||||||
url: `/user/cloud?time=${Date.now()}&cookie=${localStorage.getItem(
|
url: `/user/cloud?time=${Date.now()}&cookie=${localStorage.getItem('cookie')}`,
|
||||||
'cookie',
|
})
|
||||||
)}`,
|
.then((res) => {
|
||||||
}).then((res) => {
|
this.songs = res.data.data || []
|
||||||
console.info(res.data)
|
})
|
||||||
_this.songs = res.data.data
|
.catch((err) => {
|
||||||
|
console.error('获取云盘数据失败:', err)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.loading = false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}).mount('#app')
|
}).mount('#app')
|
||||||
const fileUpdateTime = {}
|
|
||||||
let fileLength = 0
|
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() {
|
function main() {
|
||||||
document
|
fileInput.addEventListener('change', function (e) {
|
||||||
.querySelector('input[type="file"]')
|
const files = this.files
|
||||||
.addEventListener('change', function (e) {
|
if (files.length === 0) return
|
||||||
console.info(this.files)
|
if (isUploading) return
|
||||||
let currentIndx = 0
|
|
||||||
fileLength = this.files.length
|
uploadFilesSequentially(Array.from(files))
|
||||||
for (const item of this.files) {
|
this.value = ''
|
||||||
currentIndx += 1
|
|
||||||
upload(item, currentIndx)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
main()
|
main()
|
||||||
|
|
||||||
function upload(file, currentIndx) {
|
async function uploadFilesSequentially(files) {
|
||||||
var formData = new FormData()
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isUploading = false
|
||||||
|
uploadBtn.classList.remove('disabled')
|
||||||
|
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')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
formData.append('songFile', file)
|
||||||
axios({
|
|
||||||
|
await axios({
|
||||||
method: 'post',
|
method: 'post',
|
||||||
url: `/cloud?time=${Date.now()}&cookie=${localStorage.getItem(
|
url: `/cloud?time=${Date.now()}&cookie=${localStorage.getItem('cookie')}`,
|
||||||
'cookie',
|
|
||||||
)}`,
|
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'multipart/form-data',
|
'Content-Type': 'multipart/form-data',
|
||||||
},
|
},
|
||||||
data: formData,
|
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,
|
||||||
})
|
})
|
||||||
.then((res) => {
|
|
||||||
console.info(`${file.name} 上传成功`)
|
updateProgress(index, '上传完成!', 100)
|
||||||
if (currentIndx >= fileLength) {
|
|
||||||
console.info('上传完毕')
|
} catch (err) {
|
||||||
}
|
console.error(`${file.name} 上传失败:`, err)
|
||||||
app.getData()
|
const errorMsg = err.response?.data?.msg || err.message || '未知错误'
|
||||||
})
|
if (err.response?.status === 413 || errorMsg.includes('PAYLOAD_TOO_LARGE')) {
|
||||||
.catch(async (err) => {
|
updateProgress(index, '文件过大,请切换到客户端直传模式', 0, true)
|
||||||
console.info(err)
|
|
||||||
console.info(fileUpdateTime)
|
|
||||||
fileUpdateTime[file.name]
|
|
||||||
? (fileUpdateTime[file.name] += 1)
|
|
||||||
: (fileUpdateTime[file.name] = 1)
|
|
||||||
if (fileUpdateTime[file.name] >= 4) {
|
|
||||||
console.error(`丢,这首歌怎么都传不上:${file.name}`)
|
|
||||||
return
|
|
||||||
} else {
|
} else {
|
||||||
console.error(`${file.name} 失败 ${fileUpdateTime[file.name]} 次`)
|
updateProgress(index, `上传失败: ${errorMsg}`, 0, true)
|
||||||
}
|
}
|
||||||
// await login()
|
}
|
||||||
upload(file, currentIndx)
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
|
<script src="https://fastly.jsdelivr.net/npm/spark-md5@3.0.2/spark-md5.min.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
网易云音乐 NodeJS API Enhanced
|
网易云音乐 NodeJS API Enhanced
|
||||||
|
|
||||||
|
最后更新于: 2026.2.15
|
||||||
|
|
||||||
## 灵感来自
|
## 灵感来自
|
||||||
|
|
||||||
[disoul/electron-cloud-music](https://github.com/disoul/electron-cloud-music)
|
[disoul/electron-cloud-music](https://github.com/disoul/electron-cloud-music)
|
||||||
@ -2766,7 +2768,7 @@ type : 地区
|
|||||||
|
|
||||||
参考: https://github.com/neteasecloudmusicapienhanced/api-enhanced/blob/main/public/cloud.html
|
参考: https://github.com/neteasecloudmusicapienhanced/api-enhanced/blob/main/public/cloud.html
|
||||||
|
|
||||||
访问地址: http://localhost:3000/cloud.html)
|
访问地址: http://localhost:3000/cloud.html
|
||||||
|
|
||||||
支持命令行调用,参考 module_example 目录下`song_upload.js`
|
支持命令行调用,参考 module_example 目录下`song_upload.js`
|
||||||
|
|
||||||
@ -2774,6 +2776,72 @@ type : 地区
|
|||||||
|
|
||||||
**调用例子 :** `/cloud`
|
**调用例子 :** `/cloud`
|
||||||
|
|
||||||
|
#### 上传模式说明
|
||||||
|
|
||||||
|
云盘上传支持两种模式:
|
||||||
|
|
||||||
|
**1. 后端代理模式 (默认)**
|
||||||
|
|
||||||
|
文件通过服务器转发到云存储,调用简单,但受服务器限制:
|
||||||
|
- Vercel Serverless Functions 限制请求体大小为 4.5MB
|
||||||
|
- 自建服务器需配置足够大的请求体限制
|
||||||
|
|
||||||
|
**2. 客户端直传模式 (推荐用于 Vercel)**
|
||||||
|
|
||||||
|
文件直接从客户端上传到云存储服务器,绕过服务器限制:
|
||||||
|
- 支持大文件上传
|
||||||
|
- 适合 Vercel、Netlify 等有请求体限制的平台
|
||||||
|
- 需要前端配合实现
|
||||||
|
|
||||||
|
#### 客户端直传相关接口
|
||||||
|
|
||||||
|
**获取上传凭证**
|
||||||
|
|
||||||
|
**接口地址 :** `/cloud/upload/token`
|
||||||
|
|
||||||
|
**必选参数 :**
|
||||||
|
- `cookie`: 网易云音乐 Cookie (在请求体中传递)
|
||||||
|
- `md5`: 文件 MD5 值
|
||||||
|
- `fileSize`: 文件大小(字节)
|
||||||
|
- `filename`: 文件名
|
||||||
|
|
||||||
|
**返回数据 :**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"data": {
|
||||||
|
"needUpload": true,
|
||||||
|
"songId": "...",
|
||||||
|
"uploadToken": "...",
|
||||||
|
"uploadUrl": "...",
|
||||||
|
"resourceId": "..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**完成上传导入**
|
||||||
|
|
||||||
|
**接口地址 :** `/cloud/upload/complete`
|
||||||
|
|
||||||
|
**必选参数 :**
|
||||||
|
- `cookie`: 网易云音乐 Cookie (在请求体中传递)
|
||||||
|
- `songId`: 歌曲 ID
|
||||||
|
- `resourceId`: 资源 ID
|
||||||
|
- `md5`: 文件 MD5
|
||||||
|
- `filename`: 文件名
|
||||||
|
|
||||||
|
**可选参数 :**
|
||||||
|
- `song`: 歌曲名
|
||||||
|
- `artist`: 艺术家
|
||||||
|
- `album`: 专辑名
|
||||||
|
|
||||||
|
#### 客户端直传流程
|
||||||
|
|
||||||
|
1. 客户端计算文件 MD5
|
||||||
|
2. 调用 `/cloud/upload/token` 获取上传凭证
|
||||||
|
3. 如果 `needUpload` 为 true,直接 PUT 文件到 `uploadUrl`
|
||||||
|
4. 调用 `/cloud/upload/complete` 完成导入
|
||||||
|
|
||||||
### 云盘歌曲信息匹配纠正
|
### 云盘歌曲信息匹配纠正
|
||||||
|
|
||||||
说明 : 登录后调用此接口,可对云盘歌曲信息匹配纠正,如需取消匹配,asid 需要传 0
|
说明 : 登录后调用此接口,可对云盘歌曲信息匹配纠正,如需取消匹配,asid 需要传 0
|
||||||
@ -3633,6 +3701,14 @@ type='1009' 获取其 id, 如`/search?keywords= 代码时间 &type=1009`
|
|||||||
|
|
||||||
**调用例子 :** `/musician/tasks/new`
|
**调用例子 :** `/musician/tasks/new`
|
||||||
|
|
||||||
|
### 音乐人黑胶会员任务
|
||||||
|
|
||||||
|
说明 : 音乐人登录后调用此接口 , 可获取音乐人黑胶会员任务。返回的数据中`missionStatus`字段为任务状态,100 表示任务完成。
|
||||||
|
|
||||||
|
**接口地址 :** `/musician/vip/tasks`
|
||||||
|
|
||||||
|
**调用例子 :** `/musician/vip/tasks`
|
||||||
|
|
||||||
### 账号云豆数
|
### 账号云豆数
|
||||||
|
|
||||||
说明 : 音乐人登录后调用此接口 , 可获取账号云豆数
|
说明 : 音乐人登录后调用此接口 , 可获取账号云豆数
|
||||||
@ -4875,6 +4951,43 @@ let data = encodeURIComponent(
|
|||||||
|
|
||||||
**调用例子:** `/vip/sign/info`
|
**调用例子:** `/vip/sign/info`
|
||||||
|
|
||||||
|
|
||||||
|
### 用户的创建歌单列表
|
||||||
|
|
||||||
|
说明 : 调用此接口, 传入用户id, 获取用户的创建歌单列表
|
||||||
|
|
||||||
|
**必选参数 :**
|
||||||
|
|
||||||
|
`uid`: 用户 id
|
||||||
|
|
||||||
|
**可选参数 :**
|
||||||
|
|
||||||
|
`limit` : 返回数量 , 默认为 100
|
||||||
|
|
||||||
|
`offset` : 偏移数量,用于分页 ,如 :( 页数 -1)\*30, 其中 30 为 limit 的值 , 默认为 0
|
||||||
|
|
||||||
|
**接口地址 :** `/user/playlist/create`
|
||||||
|
|
||||||
|
**调用例子 :** `/user/playlist/create?uid=32953014`
|
||||||
|
|
||||||
|
### 用户的收藏歌单列表
|
||||||
|
|
||||||
|
说明 : 调用此接口, 传入用户id, 获取用户的收藏歌单列表
|
||||||
|
|
||||||
|
**必选参数 :**
|
||||||
|
|
||||||
|
`uid`: 用户 id
|
||||||
|
|
||||||
|
**可选参数 :**
|
||||||
|
|
||||||
|
`limit` : 返回数量 , 默认为 100
|
||||||
|
|
||||||
|
`offset` : 偏移数量,用于分页 ,如 :( 页数 -1)\*30, 其中 30 为 limit 的值 , 默认为 0
|
||||||
|
|
||||||
|
**接口地址 :** `/user/playlist/collect`
|
||||||
|
|
||||||
|
**调用例子 :** `/user/playlist/collect?uid=32953014`
|
||||||
|
|
||||||
## 离线访问此文档
|
## 离线访问此文档
|
||||||
|
|
||||||
此文档同时也是 Progressive Web Apps(PWA), 加入了 serviceWorker, 可离线访问
|
此文档同时也是 Progressive Web Apps(PWA), 加入了 serviceWorker, 可离线访问
|
||||||
|
|||||||
@ -36,12 +36,12 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<!-- Global site tag (gtag.js) - Google Analytics -->
|
<!-- Global site tag (gtag.js) - Google Analytics -->
|
||||||
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-139996012-1"></script>
|
<script async src="https://www.googletagmanager.com/gtag/js?id=G-BPRGR23JEG"></script>
|
||||||
<script>
|
<script>
|
||||||
window.dataLayer = window.dataLayer || [];
|
window.dataLayer = window.dataLayer || [];
|
||||||
function gtag() { dataLayer.push(arguments); }
|
function gtag() { dataLayer.push(arguments); }
|
||||||
gtag('js', new Date());
|
gtag('js', new Date());
|
||||||
|
|
||||||
gtag('config', 'UA-139996012-1');
|
gtag('config', 'G-BPRGR23JEG');
|
||||||
</script>
|
</script>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -1,50 +1,181 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="zh">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>eapi 参数和返回内容解析</title>
|
<title>eapi 参数和返回内容解析</title>
|
||||||
<link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet">
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<style>
|
<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:hover {
|
||||||
|
background: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-section {
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-section label {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.decode-result {
|
.decode-result {
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
background-color: #f0f0f0;
|
background: #f9f9f9;
|
||||||
padding: 10px;
|
padding: 16px;
|
||||||
border-radius: 5px;
|
border-radius: 6px;
|
||||||
margin-top: 10px;
|
border: 1px solid #eee;
|
||||||
height: 300px;
|
min-height: 200px;
|
||||||
|
max-height: 400px;
|
||||||
overflow: auto;
|
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>
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div id="app" class="p-5 flex flex-col">
|
<div id="app" class="container">
|
||||||
<h1 class="text-2xl font-bold mb-5">eapi 参数和返回内容解析</h1>
|
<h1>eapi 参数和返回内容解析</h1>
|
||||||
<textarea class="border border-gray-300 p-3 mb-5" v-model="hexString" rows="10"></textarea>
|
|
||||||
<button @click="decrypt" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
|
<div class="form-group">
|
||||||
解密
|
<label for="hexString">十六进制字符串</label>
|
||||||
</button>
|
<textarea id="hexString" v-model="hexString" rows="10"></textarea>
|
||||||
<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>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
<div>
|
<button @click="decrypt">解密</button>
|
||||||
<p>使用例子:</p>
|
|
||||||
<img src="/static/eapi_params.png" />
|
<div class="result-section">
|
||||||
<img src="/static/eapi_response.png" />
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -64,6 +195,13 @@
|
|||||||
this.decrypt()
|
this.decrypt()
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
formatResult(result) {
|
||||||
|
try {
|
||||||
|
return JSON.stringify(JSON.parse(result), null, 2)
|
||||||
|
} catch (e) {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
},
|
||||||
async decrypt() {
|
async decrypt() {
|
||||||
try {
|
try {
|
||||||
const res = await axios({
|
const res = await axios({
|
||||||
@ -77,7 +215,7 @@
|
|||||||
console.log(res.data);
|
console.log(res.data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
alert(error?.response?.data?.message || '解密失败,数据格式错误')
|
alert(error?.response?.data?.message || '解密失败,数据格式错误')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,33 +7,46 @@
|
|||||||
<title>网易云音乐 API Enhanced</title>
|
<title>网易云音乐 API Enhanced</title>
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
--fg: #111827; /* gray-900 */
|
--fg: #333;
|
||||||
--muted: #6b7280; /* gray-500 */
|
--muted: #666;
|
||||||
--border: #e5e7eb; /* gray-200 */
|
--border: #ddd;
|
||||||
--bg: #ffffff;
|
--bg: #f5f5f5;
|
||||||
--panel: #f9fafb; /* gray-50 */
|
--panel: #ffffff;
|
||||||
--accent: #2563eb; /* blue-600 */
|
--accent: #333;
|
||||||
}
|
}
|
||||||
* { box-sizing: border-box; }
|
* { box-sizing: border-box; }
|
||||||
html, body { height: 100%; }
|
html, body { height: 100%; }
|
||||||
body { margin: 0; font-family: system-ui, -apple-system, Segoe UI, PingFang SC, Helvetica, Arial, sans-serif; color: var(--fg); background: var(--bg); line-height: 1.6; }
|
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; }
|
.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; }
|
||||||
|
}
|
||||||
header.site-header { margin-bottom: 24px; }
|
header.site-header { margin-bottom: 24px; }
|
||||||
header.site-header h1 { font-size: 28px; font-weight: 700; margin: 0; }
|
header.site-header h1 { font-size: 28px; font-weight: 600; margin: 0; }
|
||||||
.badge { display: inline-block; margin-left: 8px; padding: 2px 8px; border: 1px solid var(--border); border-radius: 14px; font-size: 12px; color: var(--muted); }
|
.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: 6px; color: var(--muted); }
|
.sub { margin-top: 8px; color: var(--muted); font-size: 14px; }
|
||||||
.block { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 16px; margin-bottom: 16px; }
|
.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 10px; font-size: 18px; }
|
.block h2 { margin: 0 0 12px; font-size: 18px; font-weight: 600; }
|
||||||
.kvs { display: grid; grid-template-columns: 140px 1fr; gap: 8px 16px; align-items: center; }
|
.kvs { display: grid; grid-template-columns: 100px 1fr; gap: 8px 12px; align-items: start; }
|
||||||
.kvs div:first-child { color: var(--muted); }
|
.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; }
|
||||||
|
}
|
||||||
ul.links { list-style: none; padding: 0; margin: 0; }
|
ul.links { list-style: none; padding: 0; margin: 0; }
|
||||||
ul.links li { margin: 6px 0; }
|
ul.links li { margin: 8px 0; }
|
||||||
ul.links a { color: var(--fg); text-decoration: none; border-bottom: 1px dotted var(--border); }
|
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); }
|
ul.links a:hover { color: var(--accent); border-bottom-color: var(--accent); }
|
||||||
pre { margin: 0; background: #fff; border: 1px solid var(--border); border-radius: 6px; padding: 12px; overflow: auto; }
|
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: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace; font-size: 13px; }
|
code { font-family: 'Courier New', monospace; font-size: 13px; }
|
||||||
footer.site-footer { margin-top: 24px; padding-top: 12px; border-top: 1px solid var(--border); color: var(--muted); }
|
@media (max-width: 480px) {
|
||||||
footer.site-footer a { color: var(--fg); text-decoration: none; }
|
code { font-size: 12px; }
|
||||||
|
}
|
||||||
|
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); }
|
footer.site-footer a:hover { color: var(--accent); }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
@ -71,7 +84,18 @@
|
|||||||
<h2>调试部分</h2>
|
<h2>调试部分</h2>
|
||||||
<pre><code>curl -s {origin}/inner/version
|
<pre><code>curl -s {origin}/inner/version
|
||||||
curl -s {origin}/search?keywords=网易云</code></pre>
|
curl -s {origin}/search?keywords=网易云</code></pre>
|
||||||
<p style="margin-top:10px"> · <a href="/api.html">交互式调试</a> · <a href="/qrlogin.html">二维码登录示例</a> · <a href="/unblock_test.html">解灰测试</a></p> · <a href="/audio_match_demo/index.html">听歌识曲 Demo</a></p> · <a href="/cloud.html">云盘上传</a></p> · <a href="/playlist_import.html">歌单导入</a></p> · <a href="/eapi_decrypt.html">EAPI 解密</p> · <a href="/listen_together_host.html">一起听示例</p> · <a href="/playlist_cover_update.html">更新歌单封面示例</p> · <a href="/avatar_update.html">头像更新示例</p>
|
<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>
|
</section>
|
||||||
|
|
||||||
<footer class="site-footer">
|
<footer class="site-footer">
|
||||||
|
|||||||
@ -1,92 +1,321 @@
|
|||||||
<!-- eslint-disable prettier/prettier -->
|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="zh">
|
<html lang="zh">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>一起听</title>
|
<title>一起听 - 主机模式</title>
|
||||||
<script src="https://unpkg.com/petite-vue"></script>
|
<script src="https://unpkg.com/petite-vue"></script>
|
||||||
<script src="https://fastly.jsdelivr.net/npm/axios@0.26.1/dist/axios.min.js"></script>
|
<script src="https://fastly.jsdelivr.net/npm/axios@0.26.1/dist/axios.min.js"></script>
|
||||||
<link
|
<style>
|
||||||
rel="stylesheet"
|
* {
|
||||||
href="https://unpkg.com/mdui@1.0.2/dist/css/mdui.min.css"
|
margin: 0;
|
||||||
/>
|
padding: 0;
|
||||||
<script src="https://unpkg.com/mdui@1.0.2/dist/js/mdui.min.js"></script>
|
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>
|
</head>
|
||||||
|
|
||||||
<body class="mdui-container">
|
<body class="container">
|
||||||
<div>
|
<a href="/qrlogin.html" class="login-link">还没登录?点击登录</a>
|
||||||
<a href="/qrlogin.html"> 如果没登录,请先登录 </a>
|
|
||||||
</div>
|
|
||||||
<h1>一起听 - 主机模式</h1>
|
<h1>一起听 - 主机模式</h1>
|
||||||
<div>消息: {{message}}</div>
|
|
||||||
<audio id="player" autoplay controls></audio>
|
<div class="message">消息: {{message}}</div>
|
||||||
<br />
|
|
||||||
<br />
|
<audio id="player" class="audio-player" autoplay controls></audio>
|
||||||
<button v-if="!account.login" @click="login">获取登录状态</button>
|
|
||||||
<div>您的当前登录账号为: {{account.nickname}}</div>
|
<div v-if="!account.login">
|
||||||
<br />
|
<button class="btn" @click="login">获取登录状态</button>
|
||||||
<div v-if="account.login">
|
</div>
|
||||||
<button v-if="!roomInfo.roomId" @click="createRoom">创建房间</button>
|
|
||||||
|
<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>
|
<details>
|
||||||
<summary>加入房间</summary>
|
<summary>加入房间</summary>
|
||||||
<div><span>房间ID: </span><input v-model="roomInfo.roomId" /></div>
|
<div class="input-group">
|
||||||
<div>
|
<label>房间ID:</label>
|
||||||
<span>邀请者 ID: </span><input v-model="roomInfo.inviterId" />
|
<input v-model="roomInfo.roomId" type="text" />
|
||||||
</div>
|
</div>
|
||||||
<button @click="joinRoom">点击加入</button>
|
<div class="input-group">
|
||||||
|
<label>邀请者ID:</label>
|
||||||
|
<input v-model="roomInfo.inviterId" type="text" />
|
||||||
|
</div>
|
||||||
|
<button class="btn" @click="joinRoom">加入房间</button>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<div v-if="roomInfo.roomId">
|
<div v-if="roomInfo.roomId" style="margin-top: 16px;">
|
||||||
<div>
|
<h4>分享链接</h4>
|
||||||
分享链接为:
|
<div class="share-link">
|
||||||
https://st.music.163.com/listen-together/share/?songId=1372188635&roomId={{roomInfo.roomId}}&inviterId={{roomInfo.inviterId}}
|
https://st.music.163.com/listen-together/share/?songId=1372188635&roomId={{roomInfo.roomId}}&inviterId={{roomInfo.inviterId}}
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<button class="btn" @click="refreshRoom">刷新房间状态</button>
|
||||||
<button @click="refreshRoom">刷新房间状态</button>
|
<button class="btn" @click="closeRoom">关闭房间</button>
|
||||||
<div>在线用户:</div>
|
|
||||||
<ul class="mdui-list">
|
<h4 style="margin-top: 16px;">在线用户</h4>
|
||||||
<li
|
<ul class="user-list">
|
||||||
v-for="user in roomInfo.roomUsers"
|
<li v-for="user in roomInfo.roomUsers" :key="user.userId" class="user-item">
|
||||||
class="mdui-list-item mdui-ripple"
|
<img :src="user.avatarUrl" class="user-avatar" alt="avatar" />
|
||||||
>
|
<span class="user-name">{{user.nickname}}</span>
|
||||||
<div class="mdui-list-item-avatar">
|
|
||||||
<img :src="user.avatarUrl" />
|
|
||||||
</div>
|
|
||||||
<div class="mdui-list-item-content">{{user.nickname}}</div>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<button v-if="roomInfo.roomId" @click="closeRoom">关闭房间</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button @click="playTrack">播放</button>
|
|
||||||
<button @click="pauseTrack">暂停</button>
|
<div class="section">
|
||||||
<button @click="seekTrack">同步进度</button>
|
<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>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>播放列表</summary>
|
<summary>播放列表</summary>
|
||||||
<br />
|
<div class="section">
|
||||||
<div>
|
<div class="input-group">
|
||||||
<span>歌单ID: </span><input v-model="playlistInfo.playlistId" />
|
<label>歌单ID:</label>
|
||||||
|
<input v-model="playlistInfo.playlistId" type="text" />
|
||||||
</div>
|
</div>
|
||||||
<button @click="loadPlaylist">加载歌单到播放列表</button>
|
<button class="btn" @click="loadPlaylist">加载歌单</button>
|
||||||
<span>{{playlistInfo.playlistName}}</span>
|
<div style="margin-top: 12px; font-size: 14px; color: #555;">
|
||||||
<br />
|
歌单名称: {{playlistInfo.playlistName}}
|
||||||
<br />
|
</div>
|
||||||
<div>歌单内容:</div>
|
|
||||||
<ul class="mdui-list">
|
<h4 style="margin-top: 16px;">歌单内容</h4>
|
||||||
|
<ul class="track-list">
|
||||||
<li
|
<li
|
||||||
@click="gotoTrack(track.id)"
|
@click="gotoTrack(track.id)"
|
||||||
v-for="track in playlistInfo.playlistTracks"
|
v-for="track in playlistInfo.playlistTracks"
|
||||||
class="mdui-list-item mdui-ripple"
|
:key="track.id"
|
||||||
|
class="track-item"
|
||||||
>
|
>
|
||||||
<div class="mdui-list-item-avatar">
|
<img :src="track.al.picUrl" class="track-cover" alt="cover" />
|
||||||
<img :src="track.al.picUrl" />
|
<span class="track-name">{{track.name}}</span>
|
||||||
</div>
|
|
||||||
<div class="mdui-list-item-content">{{track.name}}</div>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
</div>
|
||||||
</details>
|
</details>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
PetiteVue.createApp({
|
PetiteVue.createApp({
|
||||||
message: '请点击获取登录状态',
|
message: '请点击获取登录状态',
|
||||||
@ -126,7 +355,7 @@
|
|||||||
this.account.userId = res.data.data.profile.userId
|
this.account.userId = res.data.data.profile.userId
|
||||||
this.account.nickname = res.data.data.profile.nickname
|
this.account.nickname = res.data.data.profile.nickname
|
||||||
this.account.login = true
|
this.account.login = true
|
||||||
this.message = '成功登录, 请创建房间'
|
this.message = '成功登录,请创建房间'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
joinRoom: async function () {
|
joinRoom: async function () {
|
||||||
@ -212,7 +441,7 @@
|
|||||||
})
|
})
|
||||||
console.info(res)
|
console.info(res)
|
||||||
if (res.data.code != 200 || !res.data.data.inRoom) {
|
if (res.data.code != 200 || !res.data.data.inRoom) {
|
||||||
this.message = '房间状态获取失败, 可能退出了房间'
|
this.message = '房间状态获取失败,可能退出了房间'
|
||||||
} else {
|
} else {
|
||||||
this.roomInfo.roomUsers = res.data.data.roomInfo.roomUsers
|
this.roomInfo.roomUsers = res.data.data.roomInfo.roomUsers
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,21 +5,143 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>登录</title>
|
<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>
|
</head>
|
||||||
|
|
||||||
<body>
|
<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/axios@0.26.1/dist/axios.min.js"></script>
|
<script src="https://fastly.jsdelivr.net/npm/axios@0.26.1/dist/axios.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
const phone = '' // 这里填手机号
|
|
||||||
const password = '' // 这里填密码
|
|
||||||
const fileUpdateTime = {}
|
const fileUpdateTime = {}
|
||||||
if (!phone || !password) {
|
|
||||||
const msg = '请设置你的手机号码和密码'
|
|
||||||
alert(msg)
|
|
||||||
throw new Error(msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function login() {
|
async function login(phone, password) {
|
||||||
const res = await axios({
|
const res = await axios({
|
||||||
url: `/login/cellphone`,
|
url: `/login/cellphone`,
|
||||||
method: 'post',
|
method: 'post',
|
||||||
@ -30,8 +152,29 @@
|
|||||||
})
|
})
|
||||||
return res.data.cookie
|
return res.data.cookie
|
||||||
}
|
}
|
||||||
async function main() {
|
|
||||||
const cookieToken = await login()
|
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({
|
const res = await axios({
|
||||||
url: `/login/status`,
|
url: `/login/status`,
|
||||||
method: 'post',
|
method: 'post',
|
||||||
@ -39,8 +182,37 @@
|
|||||||
cookie: cookieToken,
|
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 = '登录'
|
||||||
}
|
}
|
||||||
main()
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|||||||
@ -5,56 +5,297 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>歌单封面上传</title>
|
<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>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div>
|
<div class="container">
|
||||||
<a href="/qrlogin-nocookie.html">
|
<h1>歌单封面上传</h1>
|
||||||
如果没登录,请先登录
|
<p class="subtitle">上传自定义歌单封面图片</p>
|
||||||
</a>
|
|
||||||
|
<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>
|
||||||
<input id="file" type="file" name="filename" />
|
|
||||||
<img id="playlist_cover" style="height: 200px; width: 200px; border-radius: 50%" />
|
<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>
|
||||||
|
|
||||||
<script src="https://fastly.jsdelivr.net/npm/axios@0.26.1/dist/axios.min.js"></script>
|
<script src="https://fastly.jsdelivr.net/npm/axios@0.26.1/dist/axios.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
const playlist_id = ''
|
const loadingOverlay = document.getElementById('loading')
|
||||||
if (!playlist_id) {
|
const playlistIdInput = document.getElementById('playlistId')
|
||||||
const msg = '请设置你的歌单id'
|
const resultDiv = document.getElementById('result')
|
||||||
alert(msg)
|
|
||||||
throw new Error(msg)
|
function showLoading() {
|
||||||
|
loadingOverlay.classList.add('active')
|
||||||
}
|
}
|
||||||
|
|
||||||
main()
|
function hideLoading() {
|
||||||
async function main() {
|
loadingOverlay.classList.remove('active')
|
||||||
document.querySelector('input[type="file"]').addEventListener(
|
}
|
||||||
'change',
|
|
||||||
function (e) {
|
function showResult(message, type) {
|
||||||
var file = this.files[0]
|
resultDiv.textContent = message
|
||||||
upload(file)
|
resultDiv.className = 'result ' + type
|
||||||
},
|
resultDiv.style.display = 'block'
|
||||||
false,
|
}
|
||||||
)
|
|
||||||
|
function hideResult() {
|
||||||
|
resultDiv.style.display = 'none'
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadPlaylistCover() {
|
||||||
|
const playlistId = playlistIdInput.value.trim()
|
||||||
|
if (!playlistId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoading()
|
||||||
|
hideResult()
|
||||||
|
|
||||||
|
try {
|
||||||
const res = await axios({
|
const res = await axios({
|
||||||
url: `/playlist/detail?id=${playlist_id}×tamp=${Date.now()}`,
|
url: `/playlist/detail?id=${playlistId}×tamp=${Date.now()}`,
|
||||||
})
|
})
|
||||||
document.querySelector('#playlist_cover').src = res.data.playlist.coverImgUrl
|
document.querySelector('#playlist_cover').src = res.data.playlist.coverImgUrl
|
||||||
|
hideResult()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载封面失败:', error)
|
||||||
|
showResult('加载封面失败,请检查歌单ID', 'error')
|
||||||
|
} finally {
|
||||||
|
hideLoading()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function upload(file) {
|
// 监听歌单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()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
async function upload(file, playlistId) {
|
||||||
var formData = new FormData()
|
var formData = new FormData()
|
||||||
formData.append('imgFile', file)
|
formData.append('imgFile', file)
|
||||||
const imgSize = await getImgSize(file)
|
const imgSize = await getImgSize(file)
|
||||||
const res = await axios({
|
const res = await axios({
|
||||||
method: 'post',
|
method: 'post',
|
||||||
url: `/playlist/cover/update?id=${playlist_id}&cookie=${localStorage.getItem('cookie')}&imgSize=${imgSize.width
|
url: `/playlist/cover/update?id=${playlistId}&cookie=${localStorage.getItem('cookie')}&imgSize=${imgSize.width}&imgX=0&imgY=0×tamp=${Date.now()}`,
|
||||||
}&imgX=0&imgY=0×tamp=${Date.now()}`,
|
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'multipart/form-data',
|
'Content-Type': 'multipart/form-data',
|
||||||
},
|
},
|
||||||
data: formData,
|
data: formData,
|
||||||
})
|
})
|
||||||
document.querySelector('#playlist_cover').src = res.data.data.url
|
document.querySelector('#playlist_cover').src = res.data.data.url
|
||||||
|
showResult('封面上传成功!', 'success')
|
||||||
}
|
}
|
||||||
|
|
||||||
function getImgSize(file) {
|
function getImgSize(file) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let reader = new FileReader()
|
let reader = new FileReader()
|
||||||
@ -68,6 +309,12 @@
|
|||||||
height: this.height,
|
height: this.height,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
image.onerror = function() {
|
||||||
|
reject(new Error('图片加载失败'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reader.onerror = function() {
|
||||||
|
reject(new Error('文件读取失败'))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,115 +2,300 @@
|
|||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>歌单导入工具</title>
|
<title>歌单导入工具</title>
|
||||||
<!-- 引入Bootstrap CSS -->
|
<style>
|
||||||
<link href="https://fastly.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
* {
|
||||||
<!-- 引入Bootstrap JS -->
|
margin: 0;
|
||||||
<script src="https://fastly.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
padding: 0;
|
||||||
<!-- 引入axios用于发送异步请求 -->
|
box-sizing: border-box;
|
||||||
<script src="https://fastly.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container mt-5">
|
<div class="container">
|
||||||
<h1 class="mb-4">歌单导入工具</h1>
|
<h1>歌单导入工具</h1>
|
||||||
<p>请选择一种导入方式并填写相关信息:</p>
|
<p class="subtitle">请选择一种导入方式并填写相关信息</p>
|
||||||
|
|
||||||
<!-- 表单开始 -->
|
<ul class="tabs" id="importTabs" role="tablist">
|
||||||
<form id="importForm" novalidate>
|
<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>
|
||||||
<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>
|
||||||
<li class="nav-item" role="presentation">
|
<li 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>
|
<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>
|
||||||
<li class="nav-item" role="presentation">
|
<li 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>
|
<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>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<!-- 选项卡面板 -->
|
<div class="tab-content active" id="importTabContent">
|
||||||
<div class="tab-content" id="importTabContent">
|
|
||||||
<!-- 元数据导入 -->
|
<!-- 元数据导入 -->
|
||||||
<div class="tab-pane fade show active" id="metadata" role="tabpanel" aria-labelledby="metadata-tab">
|
<div class="tab-content active" id="metadata" role="tabpanel" aria-labelledby="metadata-tab">
|
||||||
<table class="table table-bordered mb-3">
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col">歌曲名称</th>
|
<th style="width: 33%">歌曲名称</th>
|
||||||
<th scope="col">艺术家</th>
|
<th style="width: 33%">艺术家</th>
|
||||||
<th scope="col">专辑</th>
|
<th style="width: 33%">专辑</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="metadataTableBody">
|
<tbody id="metadataTableBody">
|
||||||
<!-- 默认添加一行 -->
|
|
||||||
<tr>
|
<tr>
|
||||||
<td><input type="text" class="form-control" name="name[]" placeholder="歌曲名称"></td>
|
<td><input type="text" name="name[]" placeholder="歌曲名称"></td>
|
||||||
<td><input type="text" class="form-control" name="artist[]" placeholder="艺术家"></td>
|
<td><input type="text" name="artist[]" placeholder="艺术家"></td>
|
||||||
<td><input type="text" class="form-control" name="album[]" placeholder="专辑"></td>
|
<td><input type="text" name="album[]" placeholder="专辑"></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<button type="button" class="btn btn-secondary mb-3" id="addMetadataRow">增加歌曲</button>
|
<button type="button" class="btn btn-secondary" id="addMetadataRow">增加歌曲</button>
|
||||||
</div>
|
</div>
|
||||||
<!-- 文字导入 -->
|
<!-- 文字导入 -->
|
||||||
<div class="tab-pane fade" id="text" role="tabpanel" aria-labelledby="text-tab">
|
<div class="tab-content" id="text" role="tabpanel" aria-labelledby="text-tab">
|
||||||
<div class="mb-3">
|
<div class="form-group">
|
||||||
<label for="textInput" class="form-label">文字:</label>
|
<label for="textInput">文字内容</label>
|
||||||
<textarea class="form-control" id="textInput" name="text" rows="5"></textarea>
|
<textarea id="textInput" name="text" rows="5" placeholder="请输入歌曲信息,每行一首歌曲"></textarea>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="form-group">
|
||||||
<label for="playlistNameInput" class="form-label">歌单名:</label>
|
<label for="playlistNameInput">歌单名称</label>
|
||||||
<input type="text" class="form-control" id="playlistNameInput" name="playlistName" placeholder="请输入歌单名">
|
<input type="text" id="playlistNameInput" name="playlistName" placeholder="请输入歌单名">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 链接导入 -->
|
<!-- 链接导入 -->
|
||||||
<div class="tab-pane fade" id="link" role="tabpanel" aria-labelledby="link-tab">
|
<div class="tab-content" id="link" role="tabpanel" aria-labelledby="link-tab">
|
||||||
<div class="mb-3">
|
<div class="form-group">
|
||||||
<label for="linkInputs" class="form-label">链接:</label>
|
<label>链接列表</label>
|
||||||
<div id="linkInputsContainer">
|
<div id="linkInputsContainer">
|
||||||
<div class="input-group mb-3">
|
<div class="input-group">
|
||||||
<input type="text" class="form-control" id="linkInput0" name="linkInput0" placeholder="请输入链接">
|
<input type="text" id="linkInput0" name="linkInput0" placeholder="请输入链接">
|
||||||
<button type="button" class="btn btn-secondary removeLinkButton" data-index="0">×</button>
|
<button type="button" class="btn btn-secondary removeLinkButton" data-index="0">×</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="btn btn-secondary mb-3" id="addLinkButton">增加链接</button>
|
<button type="button" class="btn btn-secondary" id="addLinkButton" style="margin-top: 8px;">增加链接</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>
|
||||||
|
<div class="form-group" style="margin-top: 20px;">
|
||||||
|
<label for="playlistNameLinkInput">歌单名称</label>
|
||||||
|
<input type="text" id="playlistNameLinkInput" name="playlistName" placeholder="请输入歌单名">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 是否导入我喜欢的音乐 -->
|
<div class="checkbox-group">
|
||||||
<div class="form-check">
|
<input type="checkbox" value="" id="importStarCheckbox">
|
||||||
<input class="form-check-input" type="checkbox" value="" id="importStarCheckbox">
|
<label for="importStarCheckbox">
|
||||||
<label class="form-check-label" for="importStarCheckbox">
|
导入"我喜欢的音乐"
|
||||||
导入“我喜欢的音乐”
|
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 提交按钮 -->
|
<button type="submit" class="btn" id="submitBtn">导入歌曲</button>
|
||||||
<button type="submit" class="btn btn-primary mt-3">导入歌曲</button>
|
</div>
|
||||||
</form>
|
|
||||||
<!-- 表单结束 -->
|
|
||||||
|
|
||||||
|
<script src="https://fastly.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
|
// 选项卡切换
|
||||||
|
const tabBtns = document.querySelectorAll('.tab-btn');
|
||||||
|
const tabContents = document.querySelectorAll('.tab-content[id]');
|
||||||
|
|
||||||
|
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() {
|
document.getElementById('addLinkButton').addEventListener('click', function() {
|
||||||
var container = document.getElementById('linkInputsContainer');
|
var container = document.getElementById('linkInputsContainer');
|
||||||
var newIndex = container.childElementCount - 1; // 减去非输入框元素的数量
|
var newIndex = container.children.length;
|
||||||
var newInput = document.createElement('input');
|
var newInput = document.createElement('input');
|
||||||
newInput.type = 'text';
|
newInput.type = 'text';
|
||||||
newInput.className = 'form-control';
|
newInput.className = '';
|
||||||
newInput.id = `linkInput${newIndex}`;
|
newInput.id = `linkInput${newIndex}`;
|
||||||
newInput.name = `linkInput${newIndex}`;
|
newInput.name = `linkInput${newIndex}`;
|
||||||
newInput.placeholder = '请输入链接';
|
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');
|
var removeButton = document.createElement('button');
|
||||||
removeButton.type = 'button';
|
removeButton.type = 'button';
|
||||||
removeButton.className = 'btn btn-secondary removeLinkButton';
|
removeButton.className = 'btn btn-secondary';
|
||||||
removeButton.textContent = '×';
|
removeButton.textContent = '×';
|
||||||
removeButton.dataset.index = newIndex.toString();
|
removeButton.dataset.index = newIndex.toString();
|
||||||
removeButton.addEventListener('click', function() {
|
removeButton.addEventListener('click', function() {
|
||||||
@ -119,7 +304,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
var inputGroup = document.createElement('div');
|
var inputGroup = document.createElement('div');
|
||||||
inputGroup.className = 'input-group mb-3';
|
inputGroup.className = 'input-group';
|
||||||
inputGroup.appendChild(newInput);
|
inputGroup.appendChild(newInput);
|
||||||
inputGroup.appendChild(removeButton);
|
inputGroup.appendChild(removeButton);
|
||||||
|
|
||||||
@ -131,37 +316,16 @@
|
|||||||
var container = document.getElementById('metadataTableBody');
|
var container = document.getElementById('metadataTableBody');
|
||||||
var newRow = document.createElement('tr');
|
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 = `
|
newRow.innerHTML = `
|
||||||
<td>${nameInput.outerHTML}</td>
|
<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>${artistInput.outerHTML}</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>${albumInput.outerHTML}</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);
|
container.appendChild(newRow);
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('importForm').addEventListener('submit', async function(event) {
|
document.getElementById('submitBtn').addEventListener('click', async function() {
|
||||||
// 阻止默认行为
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
// 获取表单值
|
// 获取表单值
|
||||||
let text = document.getElementById('textInput').value;
|
let text = document.getElementById('textInput').value;
|
||||||
let links = [];
|
let links = [];
|
||||||
@ -169,7 +333,7 @@
|
|||||||
let playlistName = '';
|
let playlistName = '';
|
||||||
|
|
||||||
// 获取所有链接输入框的值
|
// 获取所有链接输入框的值
|
||||||
let linkInputs = document.querySelectorAll('#linkInputsContainer .input-group .form-control');
|
let linkInputs = document.querySelectorAll('#linkInputsContainer .input-group input[type="text"]');
|
||||||
linkInputs.forEach(function(input) {
|
linkInputs.forEach(function(input) {
|
||||||
if (input.value.trim() !== '') {
|
if (input.value.trim() !== '') {
|
||||||
links.push(input.value);
|
links.push(input.value);
|
||||||
@ -229,15 +393,7 @@
|
|||||||
|
|
||||||
let taskId = res.data?.data?.taskId
|
let taskId = res.data?.data?.taskId
|
||||||
if (taskId) {
|
if (taskId) {
|
||||||
alert(`任务创建成功! 正在导入, 请稍等; 任务id:${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) {
|
} catch (error) {
|
||||||
console.error('Error:', error);
|
console.error('Error:', error);
|
||||||
@ -257,6 +413,5 @@
|
|||||||
// 初始化时设置歌单名输入框的状态
|
// 初始化时设置歌单名输入框的状态
|
||||||
document.getElementById('importStarCheckbox').dispatchEvent(new Event('change'));
|
document.getElementById('importStarCheckbox').dispatchEvent(new Event('change'));
|
||||||
</script>
|
</script>
|
||||||
</div>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@ -5,45 +5,164 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>二维码登录</title>
|
<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>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<img id="qrImg" />
|
<div class="container">
|
||||||
<div id="info" class="info"></div>
|
<h1>二维码登录</h1>
|
||||||
<script src="https://fastly.jsdelivr.net/npm/axios@0.26.1/dist/axios.min.js
|
<p class="subtitle">使用网易云音乐App扫描二维码登录</p>
|
||||||
"></script>
|
|
||||||
<script>
|
|
||||||
|
|
||||||
|
<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/axios@0.26.1/dist/axios.min.js"></script>
|
||||||
|
<script>
|
||||||
async function login() {
|
async function login() {
|
||||||
let timer
|
let timer
|
||||||
let timestamp = Date.now()
|
const statusDiv = document.getElementById('status')
|
||||||
const cookie = localStorage.getItem('cookie')
|
const cookie = localStorage.getItem('cookie')
|
||||||
|
|
||||||
|
updateStatus('加载二维码...', 'waiting')
|
||||||
getLoginStatus(cookie)
|
getLoginStatus(cookie)
|
||||||
|
|
||||||
|
try {
|
||||||
const res = await axios({
|
const res = await axios({
|
||||||
url: `/login/qr/key?timestamp=${Date.now()}`,
|
url: `/login/qr/key?timestamp=${Date.now()}`,
|
||||||
})
|
})
|
||||||
const key = res.data.data.unikey
|
const key = res.data.data.unikey
|
||||||
|
|
||||||
const res2 = await axios({
|
const res2 = await axios({
|
||||||
url: `/login/qr/create?key=${key}&platform=web&qrimg=true×tamp=${Date.now()}`,
|
url: `/login/qr/create?key=${key}&platform=web&qrimg=true×tamp=${Date.now()}`,
|
||||||
})
|
})
|
||||||
document.querySelector('#qrImg').src = res2.data.data.qrimg
|
document.querySelector('#qrImg').src = res2.data.data.qrimg
|
||||||
|
updateStatus('请扫描二维码', 'waiting')
|
||||||
|
|
||||||
timer = setInterval(async () => {
|
timer = setInterval(async () => {
|
||||||
const statusRes = await checkStatus(key)
|
const statusRes = await checkStatus(key)
|
||||||
if (statusRes.code === 800) {
|
if (statusRes.code === 800) {
|
||||||
alert('二维码已过期,请重新获取')
|
updateStatus('二维码已过期,请刷新页面', 'error')
|
||||||
clearInterval(timer)
|
clearInterval(timer)
|
||||||
}
|
} else if (statusRes.code === 801) {
|
||||||
if (statusRes.code === 803) {
|
updateStatus('二维码已扫描,请在手机上确认', 'waiting')
|
||||||
// 这一步会返回cookie
|
} else if (statusRes.code === 802) {
|
||||||
|
updateStatus('登录成功,正在保存信息...', 'waiting')
|
||||||
|
} else if (statusRes.code === 803) {
|
||||||
clearInterval(timer)
|
clearInterval(timer)
|
||||||
alert('授权登录成功')
|
updateStatus('授权登录成功!', 'success')
|
||||||
await getLoginStatus(statusRes.cookie)
|
await getLoginStatus(statusRes.cookie)
|
||||||
localStorage.setItem('cookie', statusRes.cookie)
|
localStorage.setItem('cookie', statusRes.cookie)
|
||||||
}
|
}
|
||||||
}, 3000)
|
}, 3000)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('登录失败:', error)
|
||||||
|
updateStatus('二维码加载失败,请刷新页面重试', 'error')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
login()
|
|
||||||
|
|
||||||
async function checkStatus(key) {
|
async function checkStatus(key) {
|
||||||
const res = await axios({
|
const res = await axios({
|
||||||
@ -51,7 +170,9 @@
|
|||||||
})
|
})
|
||||||
return res.data
|
return res.data
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getLoginStatus(cookie = '') {
|
async function getLoginStatus(cookie = '') {
|
||||||
|
try {
|
||||||
const res = await axios({
|
const res = await axios({
|
||||||
url: `/login/status?timestamp=${Date.now()}`,
|
url: `/login/status?timestamp=${Date.now()}`,
|
||||||
method: 'post',
|
method: 'post',
|
||||||
@ -59,14 +180,20 @@
|
|||||||
cookie,
|
cookie,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
document.querySelector('#info').innerText = JSON.stringify(res.data, null, 2)
|
document.querySelector('#info').textContent = JSON.stringify(res.data, null, 2)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取登录状态失败:', error)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateStatus(message, type) {
|
||||||
|
const statusDiv = document.getElementById('status')
|
||||||
|
statusDiv.textContent = message
|
||||||
|
statusDiv.className = 'status ' + type
|
||||||
|
}
|
||||||
|
|
||||||
|
login()
|
||||||
</script>
|
</script>
|
||||||
<style>
|
|
||||||
.info {
|
|
||||||
white-space: pre;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -5,44 +5,164 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>二维码登录</title>
|
<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>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<img id="qrImg" />
|
<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>
|
<div id="info" class="info"></div>
|
||||||
<script src="https://fastly.jsdelivr.net/npm/axios@0.26.1/dist/axios.min.js
|
|
||||||
"></script>
|
<p class="hint">请打开网易云音乐App,扫描上方二维码完成登录</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://fastly.jsdelivr.net/npm/axios@0.26.1/dist/axios.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
async function login() {
|
async function login() {
|
||||||
let timer
|
let timer
|
||||||
let timestamp = Date.now()
|
const statusDiv = document.getElementById('status')
|
||||||
const cookie = localStorage.getItem('cookie')
|
const cookie = localStorage.getItem('cookie')
|
||||||
|
|
||||||
|
updateStatus('加载二维码...', 'waiting')
|
||||||
getLoginStatus(cookie)
|
getLoginStatus(cookie)
|
||||||
|
|
||||||
|
try {
|
||||||
const res = await axios({
|
const res = await axios({
|
||||||
url: `/login/qr/key?timestamp=${Date.now()}`,
|
url: `/login/qr/key?timestamp=${Date.now()}`,
|
||||||
})
|
})
|
||||||
const key = res.data.data.unikey
|
const key = res.data.data.unikey
|
||||||
|
|
||||||
const res2 = await axios({
|
const res2 = await axios({
|
||||||
url: `/login/qr/create?key=${key}&platform=web&qrimg=true×tamp=${Date.now()}&ua=pc`,
|
url: `/login/qr/create?key=${key}&platform=web&qrimg=true×tamp=${Date.now()}&ua=pc`,
|
||||||
})
|
})
|
||||||
document.querySelector('#qrImg').src = res2.data.data.qrimg
|
document.querySelector('#qrImg').src = res2.data.data.qrimg
|
||||||
|
updateStatus('请扫描二维码', 'waiting')
|
||||||
|
|
||||||
timer = setInterval(async () => {
|
timer = setInterval(async () => {
|
||||||
const statusRes = await checkStatus(key)
|
const statusRes = await checkStatus(key)
|
||||||
if (statusRes.code === 800) {
|
if (statusRes.code === 800) {
|
||||||
alert('二维码已过期,请重新获取')
|
updateStatus('二维码已过期,请刷新页面', 'error')
|
||||||
clearInterval(timer)
|
clearInterval(timer)
|
||||||
}
|
} else if (statusRes.code === 801) {
|
||||||
if (statusRes.code === 803) {
|
updateStatus('二维码已扫描,请在手机上确认', 'waiting')
|
||||||
// 这一步会返回cookie
|
} else if (statusRes.code === 802) {
|
||||||
|
updateStatus('登录成功,正在保存信息...', 'waiting')
|
||||||
|
} else if (statusRes.code === 803) {
|
||||||
clearInterval(timer)
|
clearInterval(timer)
|
||||||
alert('授权登录成功')
|
updateStatus('授权登录成功!', 'success')
|
||||||
await getLoginStatus(statusRes.cookie)
|
await getLoginStatus(statusRes.cookie)
|
||||||
localStorage.setItem('cookie', statusRes.cookie)
|
localStorage.setItem('cookie', statusRes.cookie)
|
||||||
}
|
}
|
||||||
}, 3000)
|
}, 3000)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('登录失败:', error)
|
||||||
|
updateStatus('二维码加载失败,请刷新页面重试', 'error')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
login()
|
|
||||||
|
|
||||||
async function checkStatus(key) {
|
async function checkStatus(key) {
|
||||||
const res = await axios({
|
const res = await axios({
|
||||||
@ -50,7 +170,9 @@
|
|||||||
})
|
})
|
||||||
return res.data
|
return res.data
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getLoginStatus(cookie = '') {
|
async function getLoginStatus(cookie = '') {
|
||||||
|
try {
|
||||||
const res = await axios({
|
const res = await axios({
|
||||||
url: `/login/status?timestamp=${Date.now()}&ua=pc`,
|
url: `/login/status?timestamp=${Date.now()}&ua=pc`,
|
||||||
method: 'post',
|
method: 'post',
|
||||||
@ -58,14 +180,20 @@
|
|||||||
cookie,
|
cookie,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
document.querySelector('#info').innerText = JSON.stringify(res.data, null, 2)
|
document.querySelector('#info').textContent = JSON.stringify(res.data, null, 2)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取登录状态失败:', error)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateStatus(message, type) {
|
||||||
|
const statusDiv = document.getElementById('status')
|
||||||
|
statusDiv.textContent = message
|
||||||
|
statusDiv.className = 'status ' + type
|
||||||
|
}
|
||||||
|
|
||||||
|
login()
|
||||||
</script>
|
</script>
|
||||||
<style>
|
|
||||||
.info {
|
|
||||||
white-space: pre;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -5,79 +5,136 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>音乐解灰测试</title>
|
<title>音乐解灰测试</title>
|
||||||
<style>
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: Arial, sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
max-width: 800px;
|
min-height: 100vh;
|
||||||
margin: 20px auto;
|
background: #f5f5f5;
|
||||||
padding: 0 20px;
|
|
||||||
}
|
|
||||||
.container {
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
border-radius: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.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 {
|
.form-group {
|
||||||
margin-bottom: 15px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
.source-options {
|
|
||||||
display: flex;
|
label {
|
||||||
flex-wrap: wrap;
|
display: block;
|
||||||
gap: 10px;
|
font-size: 14px;
|
||||||
margin-bottom: 15px;
|
font-weight: 500;
|
||||||
|
color: #555;
|
||||||
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
.source-option {
|
|
||||||
display: flex;
|
input {
|
||||||
align-items: center;
|
width: 100%;
|
||||||
gap: 5px;
|
padding: 12px 14px;
|
||||||
}
|
|
||||||
button {
|
|
||||||
background-color: #4CAF50;
|
|
||||||
color: white;
|
|
||||||
padding: 10px 20px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
button:hover {
|
|
||||||
background-color: #45a049;
|
|
||||||
}
|
|
||||||
#result {
|
|
||||||
margin-top: 20px;
|
|
||||||
padding: 10px;
|
|
||||||
border: 1px solid #ddd;
|
border: 1px solid #ddd;
|
||||||
border-radius: 4px;
|
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;
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
min-height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1>音乐解灰测试</h1>
|
<h1>音乐解灰测试</h1>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="songId">音乐 ID:</label>
|
<label for="songId">音乐 ID</label>
|
||||||
<input type="number" id="songId" placeholder="请输入音乐ID" required>
|
<input type="number" id="songId" placeholder="请输入音乐ID" />
|
||||||
|
<div class="hint">例如: 1372188635</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="sources">音源列表:</label>
|
<label for="sources">音源列表(可选)</label>
|
||||||
<input type="text" id="sources" placeholder="请输入音源(非必填)">
|
<input type="text" id="sources" placeholder="请输入音源" />
|
||||||
|
<div class="hint">例如: kuwo, kugou, migu</div>
|
||||||
</div>
|
</div>
|
||||||
<button onclick="testSong()">开始测试</button>
|
|
||||||
|
<button id="testBtn" onclick="testSong()">开始测试</button>
|
||||||
|
|
||||||
<div id="result"></div>
|
<div id="result"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
async function testSong() {
|
async function testSong() {
|
||||||
const songId = document.getElementById('songId').value;
|
const songId = document.getElementById('songId').value;
|
||||||
|
const sources = document.getElementById('sources').value;
|
||||||
|
const testBtn = document.getElementById('testBtn');
|
||||||
|
const resultDiv = document.getElementById('result');
|
||||||
|
|
||||||
if (!songId) {
|
if (!songId) {
|
||||||
alert('请输入音乐ID');
|
alert('请输入音乐ID');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sources = document.getElementById('sources').value;
|
testBtn.disabled = true;
|
||||||
|
testBtn.textContent = '测试中...';
|
||||||
|
|
||||||
|
|
||||||
const resultDiv = document.getElementById('result');
|
|
||||||
resultDiv.textContent = '正在请求...';
|
resultDiv.textContent = '正在请求...';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -86,6 +143,9 @@
|
|||||||
resultDiv.textContent = JSON.stringify(data, null, 2);
|
resultDiv.textContent = JSON.stringify(data, null, 2);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
resultDiv.textContent = `请求失败: ${error.message}`;
|
resultDiv.textContent = `请求失败: ${error.message}`;
|
||||||
|
} finally {
|
||||||
|
testBtn.disabled = false;
|
||||||
|
testBtn.textContent = '开始测试';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -4,36 +4,230 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>播客上传声音</title>
|
<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: 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>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div>
|
<div class="container">
|
||||||
<a href="/qrlogin-nocookie.html"> 如果没登录,请先登录 </a>
|
<h1>播客上传声音</h1>
|
||||||
</div>
|
<a href="/qrlogin-nocookie.html" class="login-link">还没登录?点击登录</a>
|
||||||
<div id="app">
|
|
||||||
<ul>
|
<div class="content">
|
||||||
<li
|
<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"
|
v-for="(item, index) in voicelist"
|
||||||
|
:key="index"
|
||||||
@click="currentVoiceIndex = index"
|
@click="currentVoiceIndex = index"
|
||||||
:class="{ active: 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"
|
||||||
>
|
>
|
||||||
<img :src="item.coverUrl" style="width: 50px; width: 50px" />
|
|
||||||
<ul>
|
|
||||||
<li v-for="(item2,index) in item.voiceListData">
|
|
||||||
{{ item2.voiceName }}
|
{{ item2.voiceName }}
|
||||||
</li>
|
</div>
|
||||||
</ul>
|
</div>
|
||||||
{{item.voiceListName}}
|
</div>
|
||||||
</li>
|
</div>
|
||||||
</ul>
|
<div v-else class="empty-state">暂无播客列表</div>
|
||||||
<input v-model="songName" placeholder="请输入声音名称" />
|
</div>
|
||||||
<input v-model="description" placeholder="请输入介绍" />
|
|
||||||
<input type="file" name="songFile" />
|
<div class="upload-section">
|
||||||
<button @click="submit">上传</button>
|
<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>
|
</div>
|
||||||
|
|
||||||
<script src="https://fastly.jsdelivr.net/npm/axios@0.26.1/dist/axios.min.js"></script>
|
<script src="https://fastly.jsdelivr.net/npm/axios@0.26.1/dist/axios.min.js"></script>
|
||||||
<script src="https://fastly.jsdelivr.net/npm/vue"></script>
|
<script src="https://fastly.jsdelivr.net/npm/vue@3"></script>
|
||||||
<script>
|
<script>
|
||||||
Vue.createApp({
|
Vue.createApp({
|
||||||
data() {
|
data() {
|
||||||
@ -43,6 +237,7 @@
|
|||||||
voicelist: [],
|
voicelist: [],
|
||||||
cookieToken: '',
|
cookieToken: '',
|
||||||
currentVoiceIndex: 0,
|
currentVoiceIndex: 0,
|
||||||
|
loading: false,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
@ -50,12 +245,6 @@
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
currentVoice() {
|
currentVoice() {
|
||||||
// {
|
|
||||||
// voiceListId: '',
|
|
||||||
// coverImgId: '',
|
|
||||||
// categoryId: '',
|
|
||||||
// secondCategoryId: '',
|
|
||||||
// }
|
|
||||||
return this.voicelist[this.currentVoiceIndex]
|
return this.voicelist[this.currentVoiceIndex]
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -63,27 +252,49 @@
|
|||||||
submit() {
|
submit() {
|
||||||
console.info('submit')
|
console.info('submit')
|
||||||
const file = document.querySelector('input[type=file]').files[0]
|
const file = document.querySelector('input[type=file]').files[0]
|
||||||
|
if (!file) {
|
||||||
|
alert('请选择文件')
|
||||||
|
return
|
||||||
|
}
|
||||||
this.upload(file)
|
this.upload(file)
|
||||||
},
|
},
|
||||||
|
|
||||||
async getData() {
|
async getData() {
|
||||||
|
this.loading = true
|
||||||
|
try {
|
||||||
const res = await axios({
|
const res = await axios({
|
||||||
url: `/voicelist/search?cookie=${localStorage.getItem('cookie')}`,
|
url: `/voicelist/search?cookie=${localStorage.getItem('cookie')}`,
|
||||||
})
|
})
|
||||||
|
|
||||||
console.info(res.data.data)
|
console.info(res.data.data)
|
||||||
this.voicelist = res.data.data.list
|
this.voicelist = res.data.data.list || []
|
||||||
this.voicelist.map(async (i) => {
|
this.voicelist.forEach(async (i) => {
|
||||||
|
try {
|
||||||
const res2 = await axios({
|
const res2 = await axios({
|
||||||
url: `/voicelist/list?voiceListId=${i.voiceListId}&limit=5`,
|
url: `/voicelist/list?voiceListId=${i.voiceListId}&limit=5`,
|
||||||
})
|
})
|
||||||
i.voiceListData = res2.data.data.list
|
i.voiceListData = res2.data.data.list || []
|
||||||
console.info(res2)
|
console.info(res2)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('获取播客详情失败:', err)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
} catch (err) {
|
||||||
|
console.error('获取播客列表失败:', err)
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
upload(file) {
|
upload(file) {
|
||||||
|
if (!this.currentVoice) {
|
||||||
|
alert('请先选择播客列表')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var formData = new FormData()
|
var formData = new FormData()
|
||||||
formData.append('songFile', file)
|
formData.append('songFile', file)
|
||||||
|
|
||||||
axios({
|
axios({
|
||||||
method: 'post',
|
method: 'post',
|
||||||
url: `/voice/upload?time=${Date.now()}&cookie=${localStorage.getItem(
|
url: `/voice/upload?time=${Date.now()}&cookie=${localStorage.getItem(
|
||||||
@ -102,26 +313,14 @@
|
|||||||
})
|
})
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
alert(`${file.name} 上传成功`)
|
alert(`${file.name} 上传成功`)
|
||||||
if (currentIndx >= fileLength) {
|
|
||||||
console.info('上传完毕')
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.catch(async (err) => {
|
.catch((err) => {
|
||||||
console.info(err)
|
console.error('上传失败:', err)
|
||||||
|
alert('上传失败,请重试')
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}).mount('#app')
|
}).mount('body')
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
|
||||||
ul li {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul li.active {
|
|
||||||
color: red;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
31
server.js
31
server.js
@ -178,10 +178,25 @@ async function consturctServer(moduleDefs) {
|
|||||||
/**
|
/**
|
||||||
* Body Parser and File Upload
|
* Body Parser and File Upload
|
||||||
*/
|
*/
|
||||||
app.use(express.json({ limit: '50mb' }))
|
const MAX_UPLOAD_SIZE_MB = 500
|
||||||
app.use(express.urlencoded({ extended: false, limit: '50mb' }))
|
const MAX_UPLOAD_SIZE_BYTES = MAX_UPLOAD_SIZE_MB * 1024 * 1024
|
||||||
|
|
||||||
app.use(fileUpload())
|
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,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cache
|
* Cache
|
||||||
@ -227,6 +242,8 @@ async function consturctServer(moduleDefs) {
|
|||||||
const moduleResponse = await moduleDef.module(query, (...params) => {
|
const moduleResponse = await moduleDef.module(query, (...params) => {
|
||||||
// 参数注入客户端IP
|
// 参数注入客户端IP
|
||||||
const obj = [...params]
|
const obj = [...params]
|
||||||
|
const options = obj[2] || {}
|
||||||
|
if (!options.randomCNIP) {
|
||||||
let ip = req.ip
|
let ip = req.ip
|
||||||
|
|
||||||
if (ip.substring(0, 7) == '::ffff:') {
|
if (ip.substring(0, 7) == '::ffff:') {
|
||||||
@ -235,11 +252,13 @@ async function consturctServer(moduleDefs) {
|
|||||||
if (ip == '::1') {
|
if (ip == '::1') {
|
||||||
ip = global.cnIp
|
ip = global.cnIp
|
||||||
}
|
}
|
||||||
logger.info('Requested from ip:', ip)
|
// logger.info('Requested from ip:', ip)
|
||||||
obj[3] = {
|
obj[2] = {
|
||||||
...obj[3],
|
...options,
|
||||||
ip,
|
ip,
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return request(...obj)
|
return request(...obj)
|
||||||
})
|
})
|
||||||
logger.info(`Request Success: ${decode(req.originalUrl)}`)
|
logger.info(`Request Success: ${decode(req.originalUrl)}`)
|
||||||
|
|||||||
88
util/fileHelper.js
Normal file
88
util/fileHelper.js
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
const fs = require('fs')
|
||||||
|
const crypto = require('crypto')
|
||||||
|
const logger = require('./logger')
|
||||||
|
|
||||||
|
function isTempFile(file) {
|
||||||
|
return !!(file && file.tempFilePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getFileSize(file) {
|
||||||
|
if (isTempFile(file)) {
|
||||||
|
const stats = await fs.promises.stat(file.tempFilePath)
|
||||||
|
return stats.size
|
||||||
|
}
|
||||||
|
return file.data ? file.data.byteLength : file.size || 0
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getFileMd5(file) {
|
||||||
|
if (file.md5) {
|
||||||
|
return file.md5
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isTempFile(file)) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const hash = crypto.createHash('md5')
|
||||||
|
const stream = fs.createReadStream(file.tempFilePath)
|
||||||
|
stream.on('data', (chunk) => hash.update(chunk))
|
||||||
|
stream.on('end', () => resolve(hash.digest('hex')))
|
||||||
|
stream.on('error', reject)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.data) {
|
||||||
|
return crypto.createHash('md5').update(file.data).digest('hex')
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('无法计算文件MD5: 缺少文件数据')
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUploadData(file) {
|
||||||
|
if (isTempFile(file)) {
|
||||||
|
return fs.createReadStream(file.tempFilePath)
|
||||||
|
}
|
||||||
|
return file.data
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cleanupTempFile(filePath) {
|
||||||
|
if (!filePath) return
|
||||||
|
try {
|
||||||
|
await fs.promises.unlink(filePath)
|
||||||
|
} catch (e) {
|
||||||
|
logger.info('临时文件清理失败:', e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readFileChunk(filePath, offset, length) {
|
||||||
|
const fd = await fs.promises.open(filePath, 'r')
|
||||||
|
const buffer = Buffer.alloc(length)
|
||||||
|
await fd.read(buffer, 0, length, offset)
|
||||||
|
await fd.close()
|
||||||
|
return buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFileExtension(filename) {
|
||||||
|
if (!filename) return 'mp3'
|
||||||
|
if (filename.includes('.')) {
|
||||||
|
return filename.split('.').pop().toLowerCase()
|
||||||
|
}
|
||||||
|
return 'mp3'
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeFilename(filename) {
|
||||||
|
if (!filename) return 'unknown'
|
||||||
|
return filename
|
||||||
|
.replace(/\.[^.]+$/, '')
|
||||||
|
.replace(/\s/g, '')
|
||||||
|
.replace(/\./g, '_')
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
isTempFile,
|
||||||
|
getFileSize,
|
||||||
|
getFileMd5,
|
||||||
|
getUploadData,
|
||||||
|
cleanupTempFile,
|
||||||
|
readFileChunk,
|
||||||
|
getFileExtension,
|
||||||
|
sanitizeFilename,
|
||||||
|
}
|
||||||
@ -1,36 +1,8 @@
|
|||||||
const logger = require('./logger')
|
const logger = require('./logger')
|
||||||
// 预先定义常量和函数引用
|
const fs = require('fs')
|
||||||
// 中国 IP 段(来源:data/ChineseIPGenerate.csv)
|
const path = require('path')
|
||||||
const chinaIPRangesRaw = [
|
|
||||||
// 开始IP, 结束IP, IP个数, 位置
|
|
||||||
['1.0.1.0', '1.0.3.255', 768, '福州'],
|
|
||||||
['1.0.8.0', '1.0.15.255', 2048, '广州'],
|
|
||||||
['1.0.32.0', '1.0.63.255', 8192, '广州'],
|
|
||||||
['1.1.0.0', '1.1.0.255', 256, '福州'],
|
|
||||||
['1.1.2.0', '1.1.63.255', 15872, '广州'],
|
|
||||||
['1.2.0.0', '1.2.2.255', 768, '北京'],
|
|
||||||
['1.2.4.0', '1.2.127.255', 31744, '广州'],
|
|
||||||
['1.3.0.0', '1.3.255.255', 65536, '广州'],
|
|
||||||
['1.4.1.0', '1.4.127.255', 32512, '广州'],
|
|
||||||
['1.8.0.0', '1.8.255.255', 65536, '北京'],
|
|
||||||
['1.10.0.0', '1.10.9.255', 2560, '福州'],
|
|
||||||
['1.10.11.0', '1.10.127.255', 29952, '广州'],
|
|
||||||
['1.12.0.0', '1.15.255.255', 262144, '上海'],
|
|
||||||
['1.18.128.0', '1.18.128.255', 256, '北京'],
|
|
||||||
['1.24.0.0', '1.31.255.255', 524288, '赤峰'],
|
|
||||||
['1.45.0.0', '1.45.255.255', 65536, '北京'],
|
|
||||||
['1.48.0.0', '1.51.255.255', 262144, '济南'],
|
|
||||||
['1.56.0.0', '1.63.255.255', 524288, '伊春'],
|
|
||||||
['1.68.0.0', '1.71.255.255', 262144, '忻州'],
|
|
||||||
['1.80.0.0', '1.95.255.255', 1048576, '北京'],
|
|
||||||
['1.116.0.0', '1.117.255.255', 131072, '上海'],
|
|
||||||
['1.119.0.0', '1.119.255.255', 65536, '北京'],
|
|
||||||
['1.180.0.0', '1.185.255.255', 393216, '桂林'],
|
|
||||||
['1.188.0.0', '1.199.255.255', 786432, '洛阳'],
|
|
||||||
['1.202.0.0', '1.207.255.255', 393216, '铜仁'],
|
|
||||||
]
|
|
||||||
|
|
||||||
// 将原始字符串段转换为数值段并计算总数(在模块初始化时完成一次)
|
// IP地址转换函数
|
||||||
function ipToInt(ip) {
|
function ipToInt(ip) {
|
||||||
const parts = ip.split('.').map(Number)
|
const parts = ip.split('.').map(Number)
|
||||||
const a = (parts[0] << 24) >>> 0
|
const a = (parts[0] << 24) >>> 0
|
||||||
@ -49,20 +21,56 @@ function intToIp(int) {
|
|||||||
].join('.')
|
].join('.')
|
||||||
}
|
}
|
||||||
|
|
||||||
const chinaIPRanges = (function buildRanges() {
|
// 解析CIDR格式的IP段
|
||||||
|
function parseCIDR(cidr) {
|
||||||
|
const [ipStr, prefixLengthStr] = cidr.split('/')
|
||||||
|
const prefixLength = parseInt(prefixLengthStr, 10)
|
||||||
|
|
||||||
|
const ipInt = ipToInt(ipStr)
|
||||||
|
const mask = (0xffffffff << (32 - prefixLength)) >>> 0
|
||||||
|
const start = (ipInt & mask) >>> 0
|
||||||
|
const end = (start | (~mask >>> 0)) >>> 0
|
||||||
|
const count = end - start + 1
|
||||||
|
|
||||||
|
return { start, end, count, cidr }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从china_ip_ranges.txt加载中国IP段(CIDR格式)
|
||||||
|
const chinaIPRanges = (function loadChinaIPRanges() {
|
||||||
|
try {
|
||||||
|
const filePath = path.join(__dirname, '../data/china_ip_ranges.txt')
|
||||||
|
const content = fs.readFileSync(filePath, 'utf-8')
|
||||||
|
const lines = content
|
||||||
|
.split('\n')
|
||||||
|
.filter((line) => line.trim() && !line.startsWith('#'))
|
||||||
|
|
||||||
const arr = []
|
const arr = []
|
||||||
let total = 0
|
let total = 0
|
||||||
for (let i = 0; i < chinaIPRangesRaw.length; i++) {
|
|
||||||
const r = chinaIPRangesRaw[i]
|
for (let i = 0; i < lines.length; i++) {
|
||||||
const start = ipToInt(r[0])
|
const line = lines[i].trim()
|
||||||
const end = ipToInt(r[1])
|
if (!line) continue
|
||||||
const count = r[2] || end - start + 1
|
|
||||||
arr.push({ start, end, count, location: r[3] || '' })
|
const range = parseCIDR(line)
|
||||||
total += count
|
arr.push(range)
|
||||||
|
total += range.count
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 按IP段大小排序,提高随机选择效率
|
||||||
|
arr.sort((a, b) => b.count - a.count)
|
||||||
|
|
||||||
// attach total for convenience
|
// attach total for convenience
|
||||||
arr.totalCount = total
|
arr.totalCount = total
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`Loaded ${arr.length} Chinese IP ranges from china_ip_ranges.txt, total ${total} IPs`,
|
||||||
|
)
|
||||||
return arr
|
return arr
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to load china_ip_ranges.txt:', error.message)
|
||||||
|
// 返回空数组,generateRandomChineseIP会使用兜底逻辑
|
||||||
|
return { totalCount: 0 }
|
||||||
|
}
|
||||||
})()
|
})()
|
||||||
const floor = Math.floor
|
const floor = Math.floor
|
||||||
const random = Math.random
|
const random = Math.random
|
||||||
@ -144,16 +152,11 @@ module.exports = {
|
|||||||
// 如果没有选中(理论上不应该发生),回退到最后一个段
|
// 如果没有选中(理论上不应该发生),回退到最后一个段
|
||||||
if (!chosen) chosen = chinaIPRanges[chinaIPRanges.length - 1]
|
if (!chosen) chosen = chinaIPRanges[chinaIPRanges.length - 1]
|
||||||
|
|
||||||
// 在段内随机生成一个 IP(使用段真实的数值范围,而非 csv 中的 count)
|
// 在段内随机生成一个 IP(使用段真实的数值范围)
|
||||||
const segSize = chosen.end - chosen.start + 1
|
const segSize = chosen.end - chosen.start + 1
|
||||||
const ipInt = chosen.start + Math.floor(random() * segSize)
|
const ipInt = chosen.start + Math.floor(random() * segSize)
|
||||||
const ip = intToIp(ipInt)
|
const ip = intToIp(ipInt)
|
||||||
logger.info(
|
logger.info('Generated Random Chinese IP:', ip, 'from CIDR:', chosen.cidr)
|
||||||
'Generated Random Chinese IP:',
|
|
||||||
ip,
|
|
||||||
'location:',
|
|
||||||
chosen.location,
|
|
||||||
)
|
|
||||||
return ip
|
return ip
|
||||||
},
|
},
|
||||||
// 生成chainId的函数
|
// 生成chainId的函数
|
||||||
|
|||||||
@ -194,11 +194,11 @@ const createRequest = (uri, data, options) => {
|
|||||||
// 根据加密方式处理
|
// 根据加密方式处理
|
||||||
switch (crypto) {
|
switch (crypto) {
|
||||||
case 'weapi':
|
case 'weapi':
|
||||||
headers['Referer'] = DOMAIN
|
headers['Referer'] = options.domain || DOMAIN
|
||||||
headers['User-Agent'] = options.ua || chooseUserAgent('weapi')
|
headers['User-Agent'] = options.ua || chooseUserAgent('weapi')
|
||||||
data.csrf_token = csrfToken
|
data.csrf_token = csrfToken
|
||||||
encryptData = encrypt.weapi(data)
|
encryptData = encrypt.weapi(data)
|
||||||
url = DOMAIN + '/weapi/' + uri.substr(5)
|
url = (options.domain || DOMAIN) + '/weapi/' + uri.substr(5)
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'linuxapi':
|
case 'linuxapi':
|
||||||
@ -206,10 +206,10 @@ const createRequest = (uri, data, options) => {
|
|||||||
options.ua || chooseUserAgent('linuxapi', 'linux')
|
options.ua || chooseUserAgent('linuxapi', 'linux')
|
||||||
encryptData = encrypt.linuxapi({
|
encryptData = encrypt.linuxapi({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: DOMAIN + uri,
|
url: (options.domain || DOMAIN) + uri,
|
||||||
params: data,
|
params: data,
|
||||||
})
|
})
|
||||||
url = DOMAIN + '/api/linux/forward'
|
url = (options.domain || DOMAIN) + '/api/linux/forward'
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'eapi':
|
case 'eapi':
|
||||||
@ -249,9 +249,9 @@ const createRequest = (uri, data, options) => {
|
|||||||
: ENCRYPT_RESPONSE,
|
: ENCRYPT_RESPONSE,
|
||||||
)
|
)
|
||||||
encryptData = encrypt.eapi(uri, data)
|
encryptData = encrypt.eapi(uri, data)
|
||||||
url = API_DOMAIN + '/eapi/' + uri.substr(5)
|
url = (options.domain || API_DOMAIN) + '/eapi/' + uri.substr(5)
|
||||||
} else if (crypto === 'api') {
|
} else if (crypto === 'api') {
|
||||||
url = API_DOMAIN + uri
|
url = (options.domain || API_DOMAIN) + uri
|
||||||
encryptData = data
|
encryptData = data
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
|||||||
@ -19,7 +19,6 @@
|
|||||||
],
|
],
|
||||||
"env": {
|
"env": {
|
||||||
"NODE_ENV": "production",
|
"NODE_ENV": "production",
|
||||||
"ENABLE_FLAC": "true",
|
"ENABLE_FLAC": "true"
|
||||||
"ENABLE_GENERAL_UNBLOCK": "false"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user