mirror of
https://github.com/NeteaseCloudMusicApiEnhanced/api-enhanced.git
synced 2026-10-20 22:51:23 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e3a1c041b6 | ||
|
|
e2324e26da | ||
|
|
f01e770c35 | ||
|
|
abb56ec014 | ||
|
|
0300c1b51e | ||
|
|
9896242333 | ||
|
|
e3812be97b | ||
|
|
e7ff0da4ba | ||
|
|
65261fa658 | ||
|
|
cb5b2e7d0f | ||
|
|
f58fd4b50d | ||
|
|
1a5f91a192 | ||
|
|
50cc26f297 | ||
|
|
c92613b19e | ||
|
|
7aacc8990b | ||
|
|
8e934789a1 | ||
|
|
d26e6b0615 | ||
|
|
a9c33de182 | ||
|
|
d0935f6e42 | ||
|
|
3e0337ab94 | ||
|
|
5cf43c48ef | ||
|
|
68567f4a14 | ||
|
|
d0d71bb4f9 | ||
|
|
ec7eff1697 | ||
|
|
1882c3c31e | ||
|
|
f21f5677e8 | ||
|
|
fc5a76d5e2 | ||
|
|
fbc34a1846 | ||
|
|
3ef5ea341b | ||
|
|
4195c41171 | ||
|
|
232cc3d916 | ||
|
|
1b4af30a49 | ||
|
|
39e0e0718d | ||
|
|
01d391ac91 | ||
|
|
85d70ba9dd | ||
|
|
3cfecbcc1f | ||
|
|
c6e9746ed6 | ||
|
|
d6961ecb4d | ||
|
|
02242eecf6 | ||
|
|
269456def3 | ||
|
|
086e379f2c | ||
|
|
38803a18ae | ||
|
|
3980d09bcd | ||
|
|
64d4847350 | ||
|
|
cf74e90c1d | ||
|
|
a366983e99 | ||
|
|
6175d55f89 | ||
|
|
15fa49a2e8 | ||
|
|
f5fe69c863 | ||
|
|
b6be789ccd | ||
|
|
0395d087d9 | ||
|
|
3f679c0d87 | ||
|
|
ba9c6deaee | ||
|
|
2c36daf07a | ||
|
|
5b5f380cd5 | ||
|
|
ff7c5518d1 | ||
|
|
bbb2b9d4a0 | ||
|
|
3188238678 | ||
|
|
8e60324452 | ||
|
|
c9909cddd3 | ||
|
|
dbdbba3f98 | ||
|
|
27aa9a01cb | ||
|
|
30e522018f | ||
|
|
7b2507e43d | ||
|
|
3647fc5fa0 | ||
|
|
8521c198d3 | ||
|
|
4d8c4daa14 | ||
|
|
2e3cf4d451 | ||
|
|
288459cddb | ||
|
|
c1b8a381f3 | ||
|
|
2dd12b88ae | ||
|
|
b6a1f9ce0a | ||
|
|
5991e41c5d | ||
|
|
f61554714c | ||
|
|
266cd8d059 | ||
|
|
89a70b14d4 | ||
|
|
a9332af453 | ||
|
|
cffe68d9ff | ||
|
|
75ec2de66f | ||
|
|
0bd023c188 | ||
|
|
2c82c9b7be | ||
|
|
1320e5780b | ||
|
|
bde1f0864b | ||
|
|
e891287f1a | ||
|
|
59241631db | ||
|
|
29ef4f68c2 | ||
|
|
ee25eb17a6 | ||
|
|
31d2ec9225 | ||
|
|
b45bdd411f | ||
|
|
c14b06f6c3 | ||
|
|
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 | ||
|
|
43d1c72f8f | ||
|
|
1d7e9cb6d8 | ||
|
|
609c910d5a | ||
|
|
2b69e9dda5 | ||
|
|
796c2803df | ||
|
|
ab00919a93 | ||
|
|
db2ca44bbc | ||
|
|
6c79f7495b | ||
|
|
fd0608adef | ||
|
|
f6fc452666 | ||
|
|
748d5da8fe | ||
|
|
6613fbf50d | ||
|
|
c8493b93ad | ||
|
|
b93578a879 | ||
|
|
de54c7fe97 | ||
|
|
2e707e2f5e | ||
|
|
2994e2fadd | ||
|
|
eb08ed4ba3 | ||
|
|
d5fedb5c54 | ||
|
|
f2f78b2774 | ||
|
|
871414f2e6 | ||
|
|
2d3f15f0e5 | ||
|
|
598dd8bd34 | ||
|
|
30bd839809 | ||
|
|
d4ee3b88f8 | ||
|
|
77eb154028 | ||
|
|
140ea858c4 | ||
|
|
f7f8c54236 | ||
|
|
bcda215a6c | ||
|
|
ca6834b83f | ||
|
|
c8fba0f7d1 | ||
|
|
546a84b1b8 | ||
|
|
205ac7292f | ||
|
|
1aba68561d | ||
|
|
bd2fdff5ae | ||
|
|
4af8323694 | ||
|
|
a5a1a91dfa | ||
|
|
ee43878834 | ||
|
|
9d18639464 | ||
|
|
fe28194445 | ||
|
|
b14cb9bb24 | ||
|
|
06739b2d6d | ||
|
|
8639c9d0f1 | ||
|
|
bc4a0d4916 | ||
|
|
4e00b94ee6 | ||
|
|
9019a2a4a5 | ||
|
|
3a10a4d753 | ||
|
|
f39aafaff4 | ||
|
|
2a8fb72d1d | ||
|
|
ea6ab4f6f7 | ||
|
|
6fe19a71e3 | ||
|
|
abd3a79ecd | ||
|
|
044ef355c1 | ||
|
|
2944ca42ba | ||
|
|
d4d465e731 | ||
|
|
f49d3cac19 | ||
|
|
2f2755ac8c | ||
|
|
f46c0d4a84 | ||
|
|
14ee86e15b | ||
|
|
5e820187a1 | ||
|
|
781299c1c5 | ||
|
|
8fc6ed1a76 | ||
|
|
8bb68accee | ||
|
|
55858d89d9 | ||
|
|
597ac1498f | ||
|
|
398f410109 | ||
|
|
97980b5ac3 | ||
|
|
65ff21526a | ||
|
|
fb052fdecd | ||
|
|
b6453a1c1b | ||
|
|
0c3da3bcb7 | ||
|
|
80fbd7a620 | ||
|
|
a4c9c0edaf | ||
|
|
19e51bc39a | ||
|
|
a914b5bc11 | ||
|
|
5eeeaeee8a | ||
|
|
1ab9def4d5 | ||
|
|
446ad4c871 | ||
|
|
316826dd81 | ||
|
|
11a240cb18 | ||
|
|
df2d0e6431 | ||
|
|
a1436e1d64 | ||
|
|
1f87ccf080 | ||
|
|
323b1014d2 | ||
|
|
d2351f6f30 | ||
|
|
dd653c07e9 | ||
|
|
173496f305 | ||
|
|
b45739b621 | ||
|
|
f63068b386 | ||
|
|
927b79030b | ||
|
|
6d6eee13b9 | ||
|
|
a7f086cf8f | ||
|
|
a38d689de4 | ||
|
|
4ffb20c4ff | ||
|
|
84c19ee237 | ||
|
|
f3d95f17b4 | ||
|
|
6d33efee47 | ||
|
|
acc458f6cb | ||
|
|
d78a20ea11 | ||
|
|
2cbd17a6d7 | ||
|
|
240d8042fb | ||
|
|
f9f6db3fee | ||
|
|
89e7c98412 | ||
|
|
16bf878135 | ||
|
|
8dcbe4db9d | ||
|
|
a1fbcf7008 | ||
|
|
c9c881ba7d | ||
|
|
0e0777d175 | ||
|
|
765734cd2d | ||
|
|
7f3be34fdd | ||
|
|
00af0c281d | ||
|
|
001f206a31 | ||
|
|
4d9045d597 | ||
|
|
e001de3efa | ||
|
|
6ae1ccd5e3 | ||
|
|
cb72918c55 | ||
|
|
ba16f2f0ab | ||
|
|
a76576af23 | ||
|
|
72526ba20d | ||
|
|
db05eddb9c | ||
|
|
fb6804364a | ||
|
|
9e4626e905 | ||
|
|
6c8f7be2a5 | ||
|
|
cf97ed45b7 | ||
|
|
1d640a789f | ||
|
|
22e47582c4 | ||
|
|
ed3959f545 | ||
|
|
771c2f9d52 | ||
|
|
f5b7fa2d26 | ||
|
|
b798f1383b | ||
|
|
6a1b7d987c | ||
|
|
db95ee1987 | ||
|
|
edb9fbba09 | ||
|
|
ede6534ebf | ||
|
|
15c6ea7266 | ||
|
|
a719631607 | ||
|
|
e7d9d0fdf2 | ||
|
|
27bba9ce55 | ||
|
|
27562092b1 | ||
|
|
053640fd33 | ||
|
|
f7ad6b7d61 | ||
|
|
da6f590632 | ||
|
|
96148385c4 | ||
|
|
348b7569b8 | ||
|
|
ea3acc0b14 | ||
|
|
09f6b862fe | ||
|
|
0535b88b24 | ||
|
|
9fe3a0f491 | ||
|
|
b73685ace3 | ||
|
|
f16c92e825 | ||
|
|
abd0c9ca91 | ||
|
|
4f4aa134d5 |
+2
-1
@@ -7,8 +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
|
||||||
|
!/pnpm-lock.yaml
|
||||||
@@ -8,31 +8,20 @@ ENABLE_PROXY = false
|
|||||||
## 代理配置
|
## 代理配置
|
||||||
PROXY_URL = "https://your-proxy-url.com/?proxy="
|
PROXY_URL = "https://your-proxy-url.com/?proxy="
|
||||||
|
|
||||||
|
### 随机IP设置
|
||||||
|
## 启用随机中国IP(默认关闭);启用后,所有请求默认使用随机中国IP,除非请求参数randomCNIP显式关闭
|
||||||
|
ENABLE_RANDOM_CN_IP = false
|
||||||
|
|
||||||
### UnblockNeteaseMusic 设置项
|
### UnblockNeteaseMusic 设置项
|
||||||
## 启用全局解灰, 无论是否调用参数都会使用解灰(推荐开启)
|
## 启用全局解灰, 无论是否调用参数都会使用解灰(不推荐开启)
|
||||||
ENABLE_GENERAL_UNBLOCK = true
|
ENABLE_GENERAL_UNBLOCK = false
|
||||||
## 歌曲启用无损音质
|
## 歌曲启用无损音质
|
||||||
ENABLE_FLAC = true
|
ENABLE_FLAC = true
|
||||||
## 启用无损音质时,是否选择音质最高的
|
## 启用无损音质时,是否选择音质最高的
|
||||||
SELECT_MAX_BR = true
|
SELECT_MAX_BR = false
|
||||||
## 音源设置
|
|
||||||
UNBLOCK_SOURCE = pyncmd,qq,kuwo,migu,kugou
|
|
||||||
## 严格按照配置音源设置顺序进行匹配
|
## 严格按照配置音源设置顺序进行匹配
|
||||||
FOLLOW_SOURCE_ORDER = true
|
FOLLOW_SOURCE_ORDER = true
|
||||||
## Cookie设置项; 推荐在Vercel等平台进行环境变量的设置或者设置github secret
|
## Cookie设置项; 推荐在Vercel等平台进行环境变量的设置或者设置github secret
|
||||||
# 网易云 cookie
|
# 网易云 cookie
|
||||||
# 格式:MUSIC_U=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
# 格式:MUSIC_U=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||||
NETEASE_COOKIE = ""
|
# NETEASE_COOKIE = ""
|
||||||
# JOOX cookie
|
|
||||||
# 格式:JOOX_COOKIE="wmid=<your_wmid>; session_key=<your_session_key>"
|
|
||||||
JOOX_COOKIE = ""
|
|
||||||
# 咪咕 cookie
|
|
||||||
# 格式:MIGU_COOKIE="<your_aversionid>"
|
|
||||||
MIGU_COOKIE = ""
|
|
||||||
# qq cookie
|
|
||||||
# 格式:QQ_COOKIE="uin=<your_uin>; qm_keyst=<your_qm_keyst>"
|
|
||||||
QQ_COOKIE = ""
|
|
||||||
# Youtube 密钥
|
|
||||||
# 格式:YOUTUBE_KEY="<your_data_api_key>"
|
|
||||||
YOUTUBE_KEY = ""
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
root: true,
|
|
||||||
|
|
||||||
ignorePatterns: ['public/'],
|
|
||||||
parserOptions: {
|
|
||||||
parser: 'babel-eslint',
|
|
||||||
ecmaVersion: 2018,
|
|
||||||
sourceType: 'module',
|
|
||||||
},
|
|
||||||
plugins: ['html'],
|
|
||||||
extends: ['plugin:prettier/recommended'],
|
|
||||||
env: {
|
|
||||||
browser: true,
|
|
||||||
node: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
rules: {
|
|
||||||
'prettier/prettier': [
|
|
||||||
'error',
|
|
||||||
{
|
|
||||||
endOfLine: 'auto',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
indent: ['error', 2, { SwitchCase: 1 }],
|
|
||||||
'space-infix-ops': ['error', { int32Hint: false }],
|
|
||||||
'key-spacing': [
|
|
||||||
2,
|
|
||||||
{
|
|
||||||
beforeColon: false,
|
|
||||||
afterColon: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'no-octal': 2,
|
|
||||||
'no-redeclare': 2,
|
|
||||||
'comma-spacing': 2,
|
|
||||||
'no-new-object': 2,
|
|
||||||
'arrow-spacing': 2,
|
|
||||||
quotes: [
|
|
||||||
2,
|
|
||||||
'single',
|
|
||||||
{
|
|
||||||
avoidEscape: true,
|
|
||||||
allowTemplateLiterals: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
overrides: [
|
|
||||||
{
|
|
||||||
files: ['**/*.ts'],
|
|
||||||
parser: '@typescript-eslint/parser',
|
|
||||||
extends: [
|
|
||||||
'plugin:@typescript-eslint/recommended', // Uses the recommended rules from the @typescript-eslint/eslint-plugin
|
|
||||||
// 'prettier/@typescript-eslint',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
custom: ["https://rewards.focalors.ltd"]
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
---
|
|
||||||
name: 创建问题反馈
|
|
||||||
about: 问题反馈
|
|
||||||
title: "<不按照模版格式或者删除模版信息将得不到处理,确认看完了调用前须知再发 issues>"
|
|
||||||
labels: ''
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 环境
|
|
||||||
- 系统/平台: <你的系统和平台>
|
|
||||||
|
|
||||||
- nodejs 版本: <你的 NodeJS 版本号>
|
|
||||||
|
|
||||||
- API版本: <运行的云音乐 API 的版本号, 对应 package.json 里面的 version>
|
|
||||||
|
|
||||||
## 出现问题
|
|
||||||
<请清晰描述出现的问题,不要使用xxx不行之类的模糊不清的语句,我没有读心术>
|
|
||||||
|
|
||||||
## 重现步骤
|
|
||||||
<请清晰描述重现步骤,调用的接口链接和参数以及命令行错误信息,最好贴上截图(命令行错误信息和接口返回内容)>
|
|
||||||
|
|
||||||
## 期待效果
|
|
||||||
<请清晰描述现在的效果和期待的效果>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
>注意: 如果解灰出现问题, 本项目只是集成[UnblockNeteaseMusic](https://github.com/UnblockNeteaseMusic/server)的接口, 请在对应的仓库开启议题
|
|
||||||
|
|
||||||
>维护项目都是业余时间,精力有限,我只能挑容易解决的issues处理,为了节约双方时间,请尽可能提供足够的有用的信息,给的信息不够我只能根据精力和时间看情况处理,如果模板信息看都不看就删掉,我不会进行任何回复,并且一个月后close掉issue
|
|
||||||
|
|
||||||
>先看文档有没有相关说明,调用前须知必看,确认看完了调用前须知再发 issues
|
|
||||||
|
|
||||||
>先在 issues 搜一下是否有相似问题,没有再发,否则直接关闭
|
|
||||||
|
|
||||||
>不处理别人搭建的线上服务的问题,此项目提供任何线上服务不保证质量,请自行搭建
|
|
||||||
|
|
||||||
>重现步骤尽量详细,不能含糊不清,包含请求地址和对应参数以及操作过程描述,不是每个人都喜欢猜别人遇到了什么问题和找参数一个个试,也比较浪费时间
|
|
||||||
|
|
||||||
>如果不是提建议,提 issues 如果不照着模版来将不会优先处理或放着不管
|
|
||||||
|
|
||||||
>460 cheating 的问题把 `utils/request.js` 里面的 `headers['X-Real-IP']` 的注释取消掉就好
|
|
||||||
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
---
|
|
||||||
name: 功能需求
|
|
||||||
about: 希望添加新接口
|
|
||||||
title: ''
|
|
||||||
labels: 功能需求
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**功能描述**
|
|
||||||
- 请清晰描述你所希望添加的功能,最好贴上对应客户端界面的截图
|
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
name: 🐛 Bug报告
|
||||||
|
description: 报告API其中的错误或问题
|
||||||
|
title: "[Bug] "
|
||||||
|
labels: ["bug"]
|
||||||
|
assignees: []
|
||||||
|
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
感谢您报告Bug!请填写以下信息帮助我们快速定位和修复问题。
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: terms
|
||||||
|
attributes:
|
||||||
|
label: 确认事项
|
||||||
|
description: |
|
||||||
|
在提交Bug报告前,请确认以下事项:
|
||||||
|
|
||||||
|
- 我已经搜索了现有的issues,确认这不是重复问题
|
||||||
|
- 我使用的是最新版本的API, 而且是官方发布的版本,而不是fork或修改版
|
||||||
|
- 不处理别人搭建的线上服务的问题,此项目提供任何线上服务不保证质量
|
||||||
|
- 如果不是提建议,提 issues 如果不照着模版来将不会优先处理或放着不管
|
||||||
|
- 维护项目都是业余时间,精力有限,我只能挑容易解决的issues处理,为了节约双方时间,请尽可能提供足够的有用的信息,给的信息不够我只能根据精力和时间看情况处理,如果模板信息看都不看就删掉,我不会进行任何回复,并且一个月后close掉issue
|
||||||
|
|
||||||
|
options:
|
||||||
|
- label: 我已确认以上事项
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: version
|
||||||
|
attributes:
|
||||||
|
label: API版本
|
||||||
|
description: 请提供您使用的API版本以及nodejs版本
|
||||||
|
placeholder: "v4.2x.x, Node.js v22.x.x"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: deployment
|
||||||
|
attributes:
|
||||||
|
label: 部署方式
|
||||||
|
description: 此项目支持本地部署和云平台部署,您使用的是哪种方式?
|
||||||
|
options:
|
||||||
|
- 本机直接运行 (node / pm2)
|
||||||
|
- 本机 Docker 部署
|
||||||
|
- Vercel
|
||||||
|
- Heroku
|
||||||
|
- Railway
|
||||||
|
- 其他云平台 (请在描述中说明)
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: bug-description
|
||||||
|
attributes:
|
||||||
|
label: Bug描述
|
||||||
|
description: 请详细描述您遇到的问题
|
||||||
|
placeholder: |
|
||||||
|
清晰简洁地描述bug的具体表现...
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: reproduction-steps
|
||||||
|
attributes:
|
||||||
|
label: 重现步骤
|
||||||
|
description: 请提供详细的重现步骤
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: expected-behavior
|
||||||
|
attributes:
|
||||||
|
label: 期望行为
|
||||||
|
description: 请描述您期望发生的行为
|
||||||
|
placeholder: 清晰简洁地描述您期望应该发生什么...
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: actual-behavior
|
||||||
|
attributes:
|
||||||
|
label: 实际行为
|
||||||
|
description: 请描述实际发生的行为
|
||||||
|
placeholder: 清晰简洁地描述实际发生了什么...
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: error-output
|
||||||
|
attributes:
|
||||||
|
label: 错误输出日志
|
||||||
|
description: 请提供完整的错误信息日志
|
||||||
|
render: shell
|
||||||
|
placeholder: |
|
||||||
|
错误信息输出...
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: additional-context
|
||||||
|
attributes:
|
||||||
|
label: 额外信息
|
||||||
|
description: 添加任何其他有助于理解问题的信息
|
||||||
|
placeholder: |
|
||||||
|
- 截图
|
||||||
|
- 相关配置文件
|
||||||
|
- 其他相关信息
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: help
|
||||||
|
attributes:
|
||||||
|
label: 愿意协助
|
||||||
|
description: 您是否愿意帮助修复这个bug?
|
||||||
|
options:
|
||||||
|
- label: 我愿意提交PR来修复这个bug
|
||||||
|
- label: 我可以提供更多测试用例
|
||||||
|
- label: 我可以帮助测试修复方案
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
blank_issues_enabled: true
|
||||||
|
contact_links:
|
||||||
|
- name: ↑请尽量使用议题模板创建议题↑
|
||||||
|
url: https://github.com/NeteaseCloudMusicApiEnhanced/api-enhanced/issues/new/choose
|
||||||
|
about: 选择适合的议题模板可以帮助我们更快地定位和解决问题
|
||||||
|
- name: 提问的艺术
|
||||||
|
url: https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/main/README-zh_CN.md
|
||||||
|
about: 默认所有 Issues 发起者均已了解此处的内容
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
name: ✨ 接口添加请求
|
||||||
|
description: 希望添加新接口
|
||||||
|
title: "[Feature] "
|
||||||
|
labels: ["enhancement"]
|
||||||
|
assignees: []
|
||||||
|
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
感谢您提出新功能建议!请填写以下信息帮助我们理解您的需求。
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: terms
|
||||||
|
attributes:
|
||||||
|
label: 确认事项
|
||||||
|
description: 在提交功能请求前,请确认以下事项
|
||||||
|
options:
|
||||||
|
- label: 我已经搜索了现有的issues,确认这不是重复请求
|
||||||
|
required: true
|
||||||
|
- label: 我已经查看了项目文档
|
||||||
|
required: true
|
||||||
|
- label: 该项目符合该项目的目标和范围
|
||||||
|
required: true
|
||||||
|
- label: 我确认我提供的接口请求是合理且有意义的
|
||||||
|
required: true
|
||||||
|
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: proposed-solution
|
||||||
|
attributes:
|
||||||
|
label: 功能描述
|
||||||
|
description: 请详细描述您希望添加的功能,最好贴上对应客户端界面的截图
|
||||||
|
placeholder: |
|
||||||
|
请详细描述您期望的功能:
|
||||||
|
- 新的网易云音乐api 接口?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: priority
|
||||||
|
attributes:
|
||||||
|
label: 优先级
|
||||||
|
description: 您认为这个功能的优先级如何?
|
||||||
|
options:
|
||||||
|
- 低 (可有可无的改进)
|
||||||
|
- 中 (会显著改善使用体验)
|
||||||
|
- 高 (严重影响了我使用)
|
||||||
|
- 紧急 (阻塞了重要工作)
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: compatibility
|
||||||
|
attributes:
|
||||||
|
label: 兼容性考虑
|
||||||
|
description: 这个功能是否会影响现有代码的兼容性?
|
||||||
|
placeholder: |
|
||||||
|
- 是否是破坏性变更?
|
||||||
|
- 是否需要语法变更?
|
||||||
|
- 对性能的影响?
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: implementation
|
||||||
|
attributes:
|
||||||
|
label: 实现意愿
|
||||||
|
description: 您是否愿意参与这个功能的实现?
|
||||||
|
options:
|
||||||
|
- label: 我愿意提交PR来实现这个功能
|
||||||
|
- label: 我可以协助设计API接口
|
||||||
|
- label: 我可以提供测试用例
|
||||||
|
- label: 我可以帮助编写文档
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# .github/dependabot.yml
|
||||||
|
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
# 更新 npm (JavaScript) 依赖
|
||||||
|
- package-ecosystem: "npm"
|
||||||
|
directory: "/" # package.json 所在的位置
|
||||||
|
schedule:
|
||||||
|
interval: "weekly" # 每周一检查
|
||||||
|
# 默认情况下,Dependabot 会自动获取最新版本。
|
||||||
|
# 你可以使用 `ignore` 来排除某些特定更新
|
||||||
|
ignore:
|
||||||
|
- dependency-name: "eslint"
|
||||||
|
# 对于 ESLint,忽略主版本 8 以上的所有更新
|
||||||
|
versions: ["> 8"]
|
||||||
|
# 自动合并小版本更新的 PR (可选)
|
||||||
|
# 可以设置针对 patch 或 minor 版本更新自动合并
|
||||||
|
commit-message:
|
||||||
|
prefix: "build(packages):" # PR 合并提交信息的前缀
|
||||||
|
|
||||||
|
# 更新 GitHub Actions 依赖
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/" # .github/workflows/ 目录所在的位置
|
||||||
|
schedule:
|
||||||
|
interval: "monthly"
|
||||||
|
|
||||||
|
# 更新 Python (pip) 依赖,如果你的项目有 requirements.txt
|
||||||
|
- package-ecosystem: "pip"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
|
||||||
|
# 如果你有多个目录(例如一个 monorepo),可以为每个目录单独配置
|
||||||
|
# - package-ecosystem: "npm"
|
||||||
|
# directory: "/packages/frontend"
|
||||||
|
# schedule:
|
||||||
|
# interval: "weekly"
|
||||||
|
# - package-ecosystem: "npm"
|
||||||
|
# directory: "/packages/backend"
|
||||||
|
# schedule:
|
||||||
|
# interval: "weekly"
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
name: Build Image
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- v*
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
if: github.event.base_ref == 'refs/heads/main'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: Build image job
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout main
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
- name: Get version
|
|
||||||
id: get_version
|
|
||||||
if: startsWith(github.ref, 'refs/tags/') && startsWith(github.repository, 'Binaryify/ncm')
|
|
||||||
run: echo "VERSION=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_OUTPUT
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v2
|
|
||||||
- name: Build and publish image
|
|
||||||
uses: elgohr/Publish-Docker-Github-Action@v5
|
|
||||||
if: startsWith(github.ref, 'refs/tags/') && startsWith(github.repository, 'Binaryify/ncm')
|
|
||||||
with:
|
|
||||||
name: binaryify/netease_cloud_music_api # dockerid/imageName
|
|
||||||
platforms: linux/arm64,linux/amd64 # 你准备构建的镜像平台
|
|
||||||
tags: latest,${{ steps.get_version.outputs.VERSION }}
|
|
||||||
username: ${{ secrets.DOCKER_USERNAME }} # docker hub userid 在setting创建secrets name=DOCKER_USERNAME value=dockerid
|
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }} # docker hub password,在setting创建secrets name=DOCKER_PASSWORD value=dockerpassword
|
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
name: Build Artifacts
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch: # 手动触发
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||||
|
include:
|
||||||
|
- os: ubuntu-latest
|
||||||
|
platform: linux
|
||||||
|
target: node18-linux-x64
|
||||||
|
output: precompiled/app
|
||||||
|
- os: windows-latest
|
||||||
|
platform: win
|
||||||
|
target: node18-win-x64
|
||||||
|
output: precompiled/app.exe
|
||||||
|
- os: macos-latest
|
||||||
|
platform: macos
|
||||||
|
target: node18-macos-x64
|
||||||
|
output: precompiled/app
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v6
|
||||||
|
with:
|
||||||
|
node-version: '18'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
npm install -g pnpm
|
||||||
|
pnpm install
|
||||||
|
|
||||||
|
- name: Build for ${{ matrix.platform }}
|
||||||
|
run: |
|
||||||
|
npm run pkg${{ matrix.platform }}
|
||||||
|
env:
|
||||||
|
PKG_TARGET: ${{ matrix.target }}
|
||||||
|
|
||||||
|
- name: Upload artifacts
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
with:
|
||||||
|
name: app-${{ matrix.platform }}
|
||||||
|
path: ${{ matrix.output }}
|
||||||
|
if-no-files-found: error
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
name: Issue Management
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types: [opened, labeled]
|
||||||
|
issue_comment:
|
||||||
|
types: [created]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
welcome-new-issues:
|
||||||
|
if: github.event.action == 'opened'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Welcome new issue
|
||||||
|
uses: peter-evans/create-or-update-comment@v5
|
||||||
|
with:
|
||||||
|
issue-number: ${{ github.event.issue.number }}
|
||||||
|
body: |
|
||||||
|
## 快速链接
|
||||||
|
|
||||||
|
在我们处理您的问题之前,您可以先查看以下资源:
|
||||||
|
|
||||||
|
- 📖 查看[项目文档](https://neteasecloudmusicapienhanced.js.org)
|
||||||
|
- 💬 加入[QQ交流群](https://qm.qq.com/q/TpeP9Uv2yk)
|
||||||
|
- 🔍 搜索[现有issues](https://github.com/neteasecloudmusicapienhanced/api-enhanced/issues) 看是否有类似问题
|
||||||
|
|
||||||
|
handle-help-wanted:
|
||||||
|
if: contains(github.event.issue.labels.*.name, 'question')
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Add help resources
|
||||||
|
uses: peter-evans/create-or-update-comment@v5
|
||||||
|
with:
|
||||||
|
issue-number: ${{ github.event.issue.number }}
|
||||||
|
body: |
|
||||||
|
🆘 需要帮助?
|
||||||
|
|
||||||
|
这里有一些可能对您有用的资源:
|
||||||
|
|
||||||
|
📚 **文档资源**
|
||||||
|
- [项目文档](https://neteasecloudmusicapienhanced.js.org)
|
||||||
|
|
||||||
|
🔍 **常见问题**
|
||||||
|
- 搜索[已关闭的issues](https://github.com/neteasecloudmusicapienhanced/api-enhanced/issues?q=is%3Aissue+is%3Aclosed) 看看是否有类似的问题已经被解答。
|
||||||
|
|
||||||
|
💬 **即时帮助**
|
||||||
|
- 加入QQ群:https://qm.qq.com/q/TpeP9Uv2yk
|
||||||
|
|
||||||
|
如果以上资源无法解决您的问题,请提供更多详细信息,我们会尽快为您解答!
|
||||||
|
|
||||||
|
stale-issues:
|
||||||
|
if: github.event.action == 'opened'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Mark stale issues
|
||||||
|
uses: actions/stale@v10
|
||||||
|
with:
|
||||||
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
stale-issue-message: |
|
||||||
|
这个issue已经30天没有活动了。
|
||||||
|
|
||||||
|
如果这个问题仍然存在,请回复此评论说明当前状态。
|
||||||
|
否则,这个issue将在7天后自动关闭。
|
||||||
|
|
||||||
|
您可以随时重新打开已关闭的issue。
|
||||||
|
stale-pr-message: |
|
||||||
|
这个PR已经30天没有活动了。
|
||||||
|
|
||||||
|
如果您仍在处理这个PR,请回复此评论。
|
||||||
|
否则,这个PR将在7天后自动关闭。
|
||||||
|
days-before-stale: 30
|
||||||
|
days-before-close: 7
|
||||||
|
stale-issue-label: "stale"
|
||||||
|
stale-pr-label: "stale"
|
||||||
|
exempt-issue-labels: "pinned,enhancement,priority:high,ongoing,fatal"
|
||||||
|
exempt-pr-labels: "pinned,priority:high,ongoing,fatal"
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
name: Node.js CI
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
branches: [ main ]
|
|
||||||
push:
|
|
||||||
branches: [ main ]
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test:
|
|
||||||
name: Test
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
node-version: [14.x, 16.x, 18.x ]
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
|
||||||
uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: ${{ matrix.node-version }}
|
|
||||||
- uses: actions/cache@v3
|
|
||||||
with:
|
|
||||||
path: ~/.npm
|
|
||||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-node-
|
|
||||||
- run: npm ci
|
|
||||||
name: Install dependencies
|
|
||||||
- name: Test
|
|
||||||
env:
|
|
||||||
NCM_API_TEST_LOGIN_COUNTRY_CODE: ${{ secrets.NCM_API_TEST_LOGIN_COUNTRY_CODE }}
|
|
||||||
NCM_API_TEST_LOGIN_PHONE: ${{ secrets.NCM_API_TEST_LOGIN_PHONE }}
|
|
||||||
NCM_API_TEST_LOGIN_PASSWORD: ${{ secrets.NCM_API_TEST_LOGIN_PASSWORD }}
|
|
||||||
run: npm test
|
|
||||||
lint:
|
|
||||||
name: Lint
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
node-version: [14.x]
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
|
||||||
uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: ${{ matrix.node-version }}
|
|
||||||
- uses: actions/cache@v3
|
|
||||||
with:
|
|
||||||
path: ~/.npm
|
|
||||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-node-
|
|
||||||
- run: npm ci
|
|
||||||
name: Install dependencies
|
|
||||||
- name: Lint
|
|
||||||
run: npm run lint
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
# .github/workflows/release.yml
|
|
||||||
name: Release
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
release:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
packages: write
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
|
|
||||||
# 发布到 NPM Registry
|
|
||||||
- name: Setup Node.js for NPM
|
|
||||||
uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: 20
|
|
||||||
registry-url: 'https://registry.npmjs.org/'
|
|
||||||
- run: npm install
|
|
||||||
- name: Publish to NPM
|
|
||||||
run: npm publish --access public
|
|
||||||
env:
|
|
||||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
||||||
|
|
||||||
# 发布到 GitHub Packages
|
|
||||||
- name: Setup Node.js for GitHub Packages
|
|
||||||
uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: 20
|
|
||||||
registry-url: 'https://npm.pkg.github.com'
|
|
||||||
- name: Update package name for GitHub Packages
|
|
||||||
run: |
|
|
||||||
json=`cat package.json`
|
|
||||||
echo "$json" | jq '.name = "@neteasecloudmusicapienhanced/api"' > package.json
|
|
||||||
- name: Publish to GitHub Packages
|
|
||||||
run: npm publish --access public
|
|
||||||
env:
|
|
||||||
NODE_AUTH_TOKEN: ${{ github.token }}
|
|
||||||
@@ -0,0 +1,315 @@
|
|||||||
|
name: Create Release on Version Change
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- package.json
|
||||||
|
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: release-${{ github.ref }}
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
detect:
|
||||||
|
name: Detect Version Change
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
outputs:
|
||||||
|
version: ${{ steps.version.outputs.value }}
|
||||||
|
should_release: ${{ steps.check.outputs.should_release }}
|
||||||
|
tag_exists: ${{ steps.tagcheck.outputs.exists }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Extract version from package.json
|
||||||
|
id: version
|
||||||
|
run: |
|
||||||
|
echo "value=$(jq -r .version package.json)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Check if version changed
|
||||||
|
id: check
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
|
||||||
|
echo "should_release=true" >> $GITHUB_OUTPUT
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if git diff "${{ github.event.before }}" "${{ github.sha }}" -- package.json | grep -q '"version"'; then
|
||||||
|
echo "should_release=true" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "should_release=false" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Check if tag already exists
|
||||||
|
id: tagcheck
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.version.outputs.value }}"
|
||||||
|
|
||||||
|
if git rev-parse "v$VERSION" >/dev/null 2>&1; then
|
||||||
|
echo "exists=true" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "exists=false" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
build:
|
||||||
|
name: Build ${{ matrix.platform }}
|
||||||
|
needs: detect
|
||||||
|
|
||||||
|
if: |
|
||||||
|
needs.detect.outputs.should_release == 'true' &&
|
||||||
|
needs.detect.outputs.tag_exists != 'true'
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- os: ubuntu-latest
|
||||||
|
platform: linux
|
||||||
|
target: node18-linux-x64
|
||||||
|
output: ncm-api-linux-x64
|
||||||
|
|
||||||
|
- os: windows-latest
|
||||||
|
platform: windows
|
||||||
|
target: node18-win-x64
|
||||||
|
output: ncm-api-win-x64.exe
|
||||||
|
|
||||||
|
- os: macos-latest
|
||||||
|
platform: macos
|
||||||
|
target: node18-macos-x64
|
||||||
|
output: ncm-api-macos-x64
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Setup pnpm
|
||||||
|
uses: pnpm/action-setup@v6
|
||||||
|
with:
|
||||||
|
version: 9
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v6
|
||||||
|
with:
|
||||||
|
node-version: 24
|
||||||
|
cache: pnpm
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Build binary
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
PKG_TARGET: ${{ matrix.target }}
|
||||||
|
run: |
|
||||||
|
mkdir -p release-artifacts
|
||||||
|
|
||||||
|
case "${{ matrix.platform }}" in
|
||||||
|
linux)
|
||||||
|
npm run pkglinux
|
||||||
|
mv precompiled/app "release-artifacts/${{ matrix.output }}"
|
||||||
|
;;
|
||||||
|
|
||||||
|
windows)
|
||||||
|
npm run pkgwin
|
||||||
|
mv precompiled/app.exe "release-artifacts/${{ matrix.output }}"
|
||||||
|
;;
|
||||||
|
|
||||||
|
macos)
|
||||||
|
npm run pkgmacos
|
||||||
|
mv precompiled/app "release-artifacts/${{ matrix.output }}"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
- name: Upload build artifact
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
with:
|
||||||
|
name: ${{ matrix.platform }}-binary
|
||||||
|
path: release-artifacts/*
|
||||||
|
|
||||||
|
release:
|
||||||
|
name: Create GitHub Release
|
||||||
|
|
||||||
|
needs:
|
||||||
|
- detect
|
||||||
|
- build
|
||||||
|
|
||||||
|
if: |
|
||||||
|
needs.detect.outputs.should_release == 'true' &&
|
||||||
|
needs.detect.outputs.tag_exists != 'true'
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
fetch-tags: true
|
||||||
|
|
||||||
|
- name: Download build artifacts
|
||||||
|
uses: actions/download-artifact@v8
|
||||||
|
with:
|
||||||
|
path: release-artifacts
|
||||||
|
merge-multiple: true
|
||||||
|
|
||||||
|
- name: List flattened artifacts
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
mkdir -p final-artifacts
|
||||||
|
cp release-artifacts/* final-artifacts/
|
||||||
|
ls -lah final-artifacts
|
||||||
|
|
||||||
|
- name: Generate release notes
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
VERSION="${{ needs.detect.outputs.version }}"
|
||||||
|
|
||||||
|
PREV_TAG=$(git tag --sort=-v:refname | head -1)
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "# Release v${VERSION}"
|
||||||
|
echo ""
|
||||||
|
echo "## 更新内容 / Changelog"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [ -n "$PREV_TAG" ]; then
|
||||||
|
echo "从 \`$PREV_TAG\` 到 \`v$VERSION\` 的提交记录:"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
git log "$PREV_TAG..HEAD" \
|
||||||
|
--no-merges \
|
||||||
|
--pretty=format:"- %s (%h)"
|
||||||
|
|
||||||
|
else
|
||||||
|
echo "首次发布,包含以下提交:"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
git log \
|
||||||
|
--no-merges \
|
||||||
|
--pretty=format:"- %s (%h)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo ""
|
||||||
|
echo "---"
|
||||||
|
echo "自动发布 via GitHub Actions"
|
||||||
|
|
||||||
|
} > release-notes.md
|
||||||
|
|
||||||
|
cat release-notes.md
|
||||||
|
|
||||||
|
- name: Create Git tag
|
||||||
|
run: |
|
||||||
|
git tag "v${{ needs.detect.outputs.version }}"
|
||||||
|
git push origin "v${{ needs.detect.outputs.version }}"
|
||||||
|
|
||||||
|
- name: Create GitHub Release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
tag_name: v${{ needs.detect.outputs.version }}
|
||||||
|
name: Release v${{ needs.detect.outputs.version }}
|
||||||
|
body_path: release-notes.md
|
||||||
|
files: final-artifacts/*
|
||||||
|
draft: false
|
||||||
|
prerelease: false
|
||||||
|
|
||||||
|
publish-docker:
|
||||||
|
name: Publish Docker Image
|
||||||
|
needs: detect
|
||||||
|
|
||||||
|
if: |
|
||||||
|
needs.detect.outputs.should_release == 'true' &&
|
||||||
|
needs.detect.outputs.tag_exists != 'true'
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Read package version
|
||||||
|
id: pkg
|
||||||
|
run: echo "VERSION=$(jq -r .version package.json)" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Log in to Docker Hub
|
||||||
|
uses: docker/login-action@v4
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Log in to GitHub Container Registry
|
||||||
|
uses: docker/login-action@v4
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and push Docker image
|
||||||
|
uses: docker/build-push-action@v7
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
moefurina/ncm-api:latest
|
||||||
|
moefurina/ncm-api:${{ env.VERSION }}
|
||||||
|
ghcr.io/neteasecloudmusicapienhanced/ncm-api:latest
|
||||||
|
ghcr.io/neteasecloudmusicapienhanced/ncm-api:${{ env.VERSION }}
|
||||||
|
platforms: linux/amd64,linux/arm64/v8
|
||||||
|
|
||||||
|
publish-npm:
|
||||||
|
name: Publish to npm
|
||||||
|
needs: detect
|
||||||
|
|
||||||
|
if: |
|
||||||
|
needs.detect.outputs.should_release == 'true' &&
|
||||||
|
needs.detect.outputs.tag_exists != 'true'
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
id-token: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- uses: pnpm/action-setup@v6
|
||||||
|
with:
|
||||||
|
version: 9
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v6
|
||||||
|
with:
|
||||||
|
node-version: 24
|
||||||
|
cache: pnpm
|
||||||
|
|
||||||
|
- run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- run: pnpm publish --access public --provenance
|
||||||
@@ -17,12 +17,12 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
# Step 1: run a standard checkout action
|
# Step 1: run a standard checkout action
|
||||||
- name: Checkout target repo
|
- name: Checkout target repo
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
# Step 2: run the sync action
|
# Step 2: run the sync action
|
||||||
- name: Sync upstream changes
|
- name: Sync upstream changes
|
||||||
id: sync
|
id: sync
|
||||||
uses: aormsby/Fork-Sync-With-Upstream-action@v3.4.1
|
uses: aormsby/Fork-Sync-With-Upstream-action@v3.4.3
|
||||||
with:
|
with:
|
||||||
upstream_sync_repo: NeteaseCloudMusicApiEnhanced/api-enhanced
|
upstream_sync_repo: NeteaseCloudMusicApiEnhanced/api-enhanced
|
||||||
upstream_sync_branch: main
|
upstream_sync_branch: main
|
||||||
|
|||||||
@@ -8,3 +8,5 @@ examples/moddef.json
|
|||||||
bin
|
bin
|
||||||
anonymous_token
|
anonymous_token
|
||||||
.vercel
|
.vercel
|
||||||
|
.env
|
||||||
|
IFLOW.md
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
language: node_js
|
language: node_js
|
||||||
|
|
||||||
node_js:
|
node_js:
|
||||||
- 12
|
- 22
|
||||||
|
|||||||
-1393
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -13,4 +13,4 @@ RUN yarn --network-timeout=100000
|
|||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
||||||
CMD [ "/sbin/tini", "--", "node", "app.js" ]
|
CMD [ "/sbin/tini", "--", "node", "app.js" ]
|
||||||
@@ -1,16 +1,51 @@
|
|||||||
# 网易云音乐 API Enhanced
|
<p align="center">
|
||||||
|
<img src="./public/static/neteaselogo.png" alt="网易云音乐 API Enhanced Logo" />
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://www.npmjs.com/package/@neteasecloudmusicapienhanced/api">
|
||||||
|
<img src="https://img.shields.io/npm/v/@neteasecloudmusicapienhanced/api?style=flat-square" alt="NPM Version" />
|
||||||
|
</a>
|
||||||
|
<a href="https://www.npmjs.com/package/@neteasecloudmusicapienhanced/api">
|
||||||
|
<img src="https://img.shields.io/npm/dm/@neteasecloudmusicapienhanced/api?style=flat-square" alt="NPM Downloads" />
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/NeteaseCloudMusicApiEnhanced/api-enhanced">
|
||||||
|
<img src="https://img.shields.io/github/license/NeteaseCloudMusicApiEnhanced/api-enhanced?style=flat-square" alt="License" />
|
||||||
|
</a>
|
||||||
|
<a href="https://nodejs.org/">
|
||||||
|
<img src="https://img.shields.io/node/v/@neteasecloudmusicapienhanced/api?style=flat-square" alt="Node Version" />
|
||||||
|
</a>
|
||||||
|
<a href="https://hub.docker.com/r/moefurina/ncm-api">
|
||||||
|
<img src="https://img.shields.io/docker/v/moefurina/ncm-api?style=flat-square" alt="Docker Version" />
|
||||||
|
</a>
|
||||||
|
<a href="https://hub.docker.com/r/moefurina/ncm-api">
|
||||||
|
<img src="https://img.shields.io/docker/pulls/moefurina/ncm-api?style=flat-square" alt="Docker Pulls" />
|
||||||
|
</a>
|
||||||
|
<a href="https://www.typescriptlang.org/">
|
||||||
|
<img src="https://img.shields.io/badge/TypeScript-Ready-blue?style=flat-square&logo=typescript" alt="TypeScript" />
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 项目简介
|
## 项目简介
|
||||||
|
|
||||||
本项目为网易云音乐第三方 Node.js API,基于社区停更的原版 API 持续维护和升级,支持丰富的音乐相关接口,适合自建服务、二次开发和多平台部署(如果原版诈尸, 我会及时同步 or 归档)。
|
网易云音乐第三方 Node.js API, 支持丰富的音乐相关接口,适合自建服务、二次开发和多平台部署
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
>
|
||||||
|
> ## 注意
|
||||||
|
>
|
||||||
|
> - 本项目是自由项目, 不带任何担保, 在基于`MIT License`的许可下, 任何人都可以自由使用、修改和分发本项目的代码, 包括用于商业目的.
|
||||||
|
> - 该项目是个合作计划, 有很多人参与了此次贡献, 前往 [Contributors](https://github.com/NeteaseCloudMusicApiEnhanced/api-enhanced/graphs/contributors) 页面查看所有贡献者
|
||||||
|
> - 原作者项目 [Binaryify/NeteaseCloudMusicApi](https://github.com/binaryify/NeteaseCloudMusicApi) 并非完全停止维护, 你可以在 [NeteaseCloudMusicApi 的 NPMJS 页面](https://www.npmjs.com/package/NeteaseCloudMusicApi) 查看最新版本信息。
|
||||||
|
> - 感谢尊重和理解!
|
||||||
|
|
||||||
## 快速开始
|
## 快速开始
|
||||||
|
|
||||||
### 环境要求
|
### 环境要求
|
||||||
|
|
||||||
- Node.js 14 及以上
|
- Node.js 18 及以上
|
||||||
- 推荐使用 pnpm 进行依赖管理
|
- 推荐使用 pnpm 进行依赖管理
|
||||||
|
|
||||||
### 安装
|
### 安装
|
||||||
@@ -34,25 +69,93 @@ set PORT=4000 && node app.js # Windows
|
|||||||
|
|
||||||
### 重要提示
|
### 重要提示
|
||||||
|
|
||||||
- 调用前请务必阅读文档的 [调用前须知](https://music-api.focalors.ltd/#/?id=%e8%b0%83%e7%94%a8%e5%89%8d%e9%a1%bb%e7%9f%a5) 部分。
|
- 调用前请务必阅读文档的 [调用前须知](https://neteasecloudmusicapienhanced.js.org/#/?id=%e8%b0%83%e7%94%a8%e5%89%8d%e9%a1%bb%e7%9f%a5) 部分。
|
||||||
- 推荐将敏感信息(如 cookie)通过部署平台的环境变量进行配置。
|
- 推荐将敏感信息(如 cookie)通过部署平台的环境变量进行配置。
|
||||||
|
|
||||||
## 在线体验与文档
|
## 在线体验与文档
|
||||||
|
|
||||||
- [在线文档](https://music-api.focalors.ltd/)
|
- [在线文档](https://neteasecloudmusicapienhanced.js.org/)
|
||||||
- [NPM 包地址](https://www.npmjs.com/package/@neteaseapireborn/api)
|
- [NPM 包地址](https://www.npmjs.com/package/@neteasecloudmusicapienhanced/api)
|
||||||
|
- [Dockerhub 主页](https://hub.docker.com/r/moefurina/ncm-api)
|
||||||
|
|
||||||
## 常见部署方式
|
## 常见部署方式
|
||||||
|
|
||||||
- 推荐参考[在线文档](https://music-api.focalors.ltd/)
|
- 推荐参考[在线文档](https://neteasecloudmusicapienhanced.js.org/)
|
||||||
|
|
||||||
### Vercel 一键部署
|
## Docker 部署说明
|
||||||
|
|
||||||
|
本项目支持通过 Docker 一键部署,无需手动安装 Node.js 或依赖。
|
||||||
|
|
||||||
|
> 注意: 在 docker 中运行的时候, 由于使用了 request 来发请求, 所以会检查几个 proxy 相关的环境变量(如下所列), 这些环境变量 会影响到 request 的代理, 详情请参考[request 的文档](https://github.com/request/request#proxies), 如果这些环境变量 指向的代理不可用, 那么就会造成错误, 所以在使用 docker 的时候一定要注意这些环境变量. 不过, 要是你在 query 中加上了 proxy 参数, 那么环境变量会被覆盖, 就会用你通过 proxy 参数提供的代理了.
|
||||||
|
|
||||||
|
request 相关的环境变量
|
||||||
|
|
||||||
|
1. http_proxy
|
||||||
|
2. https_proxy
|
||||||
|
3. HTTP_PROXY
|
||||||
|
4. HTTPS_PROXY
|
||||||
|
5. no_proxy
|
||||||
|
6. NO_PROXY
|
||||||
|
|
||||||
|
```shell
|
||||||
|
docker pull moefurina/ncm-api:latest
|
||||||
|
|
||||||
|
docker run -d -p 3000:3000 --name ncm-api moefurina/ncm-api:latest
|
||||||
|
|
||||||
|
|
||||||
|
// 或者
|
||||||
|
docker run -d -p 3000:3000 moefurina/ncm-api:latest
|
||||||
|
|
||||||
|
// 去掉或者设置相关的环境变量
|
||||||
|
|
||||||
|
docker run -d -p 3000:3000 --name ncm-api -e http_proxy= -e https_proxy= -e no_proxy= -e HTTP_PROXY= -e HTTPS_PROXY= -e NO_PROXY= moefurina/ncm-api:latest
|
||||||
|
|
||||||
|
// 或者
|
||||||
|
docker run -d -p 3000:3000 -e http_proxy= -e https_proxy= -e no_proxy= -e HTTP_PROXY= -e HTTPS_PROXY= -e NO_PROXY= moefurina/ncm-api:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
> 以下是自行 build docker 镜像方式
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ git clone https://github.com/NeteaseCloudMusicApiEnhanced/api-enhanced && cd api-enhanced
|
||||||
|
|
||||||
|
$ sudo docker build . -t ncm-api
|
||||||
|
|
||||||
|
$ sudo docker run -d -p 3000:3000 ncm-api
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. 环境变量
|
||||||
|
|
||||||
|
| 变量名 | 默认值 | 说明 |
|
||||||
|
|----------------------------|--------------------------------------|----------------------------------------------------|
|
||||||
|
| **CORS_ALLOW_ORIGIN** | `*` | 允许跨域请求的域名。可填写单个源,或使用逗号分隔多个源(例如 `https://a.com,https://b.com`)。 |
|
||||||
|
| **ENABLE_PROXY** | `false` | 是否启用反向代理功能。 |
|
||||||
|
| **PROXY_URL** | `https://your-proxy-url.com/?proxy=` | 代理服务地址。仅当 `ENABLE_PROXY=true` 时生效。 |
|
||||||
|
| **ENABLE_RANDOM_CN_IP** | `false` | 是否默认启用随机中国IP。启用后,所有请求默认使用随机中国IP,除非请求参数 `randomCNIP` 显式关闭。 |
|
||||||
|
| **ENABLE_GENERAL_UNBLOCK** | `true` | 是否启用全局解灰(推荐开启)。开启后所有歌曲都尝试自动解锁。 |
|
||||||
|
| **ENABLE_FLAC** | `true` | 是否启用无损音质(FLAC)。 |
|
||||||
|
| **SELECT_MAX_BR** | `false` | 启用无损音质时,是否选择最高码率音质。 |
|
||||||
|
| **FOLLOW_SOURCE_ORDER** | `true` | 是否严格按照音源列表顺序进行匹配。 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. 更新镜像
|
||||||
|
|
||||||
|
更新到最新版本:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker pull moefurina/ncm-api:latest
|
||||||
|
docker stop ncm-api && docker rm ncm-api
|
||||||
|
docker run -d -p 3000:3000 moefurina/ncm-api:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
## Vercel 一键部署
|
||||||
|
|
||||||
1. fork 本项目
|
1. fork 本项目
|
||||||
2. 在 Vercel 官网新建项目,导入 fork 的仓库
|
2. 在 Vercel 官网新建项目,导入 fork 的仓库
|
||||||
3. 直接 Deploy
|
3. 直接 Deploy
|
||||||
|
|
||||||
### 腾讯云 Serverless 部署
|
## 腾讯云 Serverless 部署
|
||||||
|
|
||||||
1. fork 本项目
|
1. fork 本项目
|
||||||
2. 在腾讯云 serverless 控制台新建 Web 应用,选择 Express 框架
|
2. 在腾讯云 serverless 控制台新建 Web 应用,选择 Express 框架
|
||||||
@@ -69,7 +172,10 @@ set PORT=4000 && node app.js # Windows
|
|||||||
支持直接在 Node.js 项目中引入和调用,返回 Promise:
|
支持直接在 Node.js 项目中引入和调用,返回 Promise:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const { login_cellphone, user_cloud } = require('@neteaseapireborn/api')
|
const {
|
||||||
|
login_cellphone,
|
||||||
|
user_cloud,
|
||||||
|
} = require('@neteasecloudmusicapienhanced/api')
|
||||||
async function main() {
|
async function main() {
|
||||||
const result = await login_cellphone({ phone: '手机号', password: '密码' })
|
const result = await login_cellphone({ phone: '手机号', password: '密码' })
|
||||||
console.log(result)
|
console.log(result)
|
||||||
@@ -82,7 +188,7 @@ main()
|
|||||||
## TypeScript 支持
|
## TypeScript 支持
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
import { banner } from '@neteaseapireborn/api'
|
import { banner } from '@neteasecloudmusicapienhanced/api'
|
||||||
banner({ type: 0 }).then((res) => console.log(res))
|
banner({ type: 0 }).then((res) => console.log(res))
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -98,22 +204,48 @@ pnpm test
|
|||||||
- 用户信息、歌单、动态、播放记录
|
- 用户信息、歌单、动态、播放记录
|
||||||
- 歌曲、专辑、歌手、MV、歌词、评论、排行榜
|
- 歌曲、专辑、歌手、MV、歌词、评论、排行榜
|
||||||
- 搜索、推荐、私人 FM、签到、云盘
|
- 搜索、推荐、私人 FM、签到、云盘
|
||||||
- 歌曲解锁(解灰)、多音源支持(qq/kuwo/kugou/migu/pyncmd)
|
- 歌曲解锁(解灰)
|
||||||
> 注意: 如果解灰出现问题, 本项目只是集成[UnblockNeteaseMusic](https://github.com/UnblockNeteaseMusic/server)的接口, 请在对应的仓库开启议题
|
|
||||||
- 详细接口请见[在线文档](https://music-api.focalors.ltd/)
|
|
||||||
|
|
||||||
## 贡献与社区
|
## 贡献与社区
|
||||||
|
|
||||||
- 欢迎提交 PR、Issue 参与维护
|
- 欢迎提交 PR、Issue 参与维护
|
||||||
|
- 要贡献新接口, 请参考[这篇文章](https://www.focalors.ltd/post/how-to-contribute-ncm-api)
|
||||||
|
|
||||||
## SDK 生态
|
### 致谢
|
||||||
|
|
||||||
| 语言 | 作者 | 地址 | 类型 |
|
原作者 [Binaryify/NeteaseCloudMusicApi](https://github.com/binaryify/NeteaseCloudMusicApi) 项目为本项目基础 (该项目在`npmjs`网站上仍持续维护, 但 github 仓库已不再更新)
|
||||||
| ------ | ------------------------------------------- | ---------------------------------------------------------------------------------------- | ------ |
|
|
||||||
|
感谢大佬们为逆向eapi, weapi, xeapi等加密算法所做的贡献
|
||||||
|
|
||||||
|
项目参考:
|
||||||
|
|
||||||
|
[trazyn/ieaseMusic](https://github.com/trazyn/ieaseMusic)
|
||||||
|
|
||||||
|
[listen1/listen1_chrome_extension](https://github.com/listen1/listen1_chrome_extension)
|
||||||
|
|
||||||
|
[chaunsin/netease-cloud-music](https://github.com/chaunsin/netease-cloud-music)
|
||||||
|
|
||||||
|
### 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) | [yumbo-music-utils](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) | 第三方 |
|
||||||
|
| Swift | [Lincb522](https://github.com/Lincb522) | [NeteaseCloudMusicApi-Swift](https://github.com/Lincb522/NeteaseCloudMusicAPI-Swift) | 第三方 |
|
||||||
|
| Rust | [SPlayer-Dev](https://github.com/splayer-dev) | [ncm-api-rs](https://github.com/SPlayer-Dev/ncm-api-rs) | 第三方 |
|
||||||
|
|
||||||
|
|
||||||
|
### 依赖此项目的优秀开源项目
|
||||||
|
|
||||||
|
- [qier2222/YesPlayMusic](https://github.com/qier222/YesPlayMusic)
|
||||||
|
|
||||||
|
- [MaigoLabs/amaoke.app](https://github.com/MaigoLabs/amaoke.app)
|
||||||
|
|
||||||
|
- [Yueby/music-together](https://github.com/Yueby/music-together)
|
||||||
|
|
||||||
|
- [chthollyphlie/folia-major](https://github.com/chthollyphile/folia-major)
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
[MIT License](https://github.com/IamFurina/NeteaseCloudMusicApiReborn/blob/main/LICENSE)
|
[MIT License](https://github.com/MoeFurina/NeteaseCloudMusicApiEnhanced/blob/main/LICENSE)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"functions": [
|
|
||||||
{
|
|
||||||
"path": "/(.*)",
|
|
||||||
"method": ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
|
||||||
"handler": "index.js",
|
|
||||||
"headers": {
|
|
||||||
"Access-Control-Allow-Credentials": "true",
|
|
||||||
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
|
|
||||||
"Access-Control-Allow-Headers": "X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
const { defineConfig, globalIgnores } = require('eslint/config')
|
||||||
|
|
||||||
|
const html = require('eslint-plugin-html')
|
||||||
|
const globals = require('globals')
|
||||||
|
const tsParser = require('@typescript-eslint/parser')
|
||||||
|
const js = require('@eslint/js')
|
||||||
|
|
||||||
|
const { FlatCompat } = require('@eslint/eslintrc')
|
||||||
|
|
||||||
|
const compat = new FlatCompat({
|
||||||
|
baseDirectory: __dirname,
|
||||||
|
recommendedConfig: js.configs.recommended,
|
||||||
|
allConfig: js.configs.all,
|
||||||
|
})
|
||||||
|
|
||||||
|
module.exports = defineConfig([
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
sourceType: 'module',
|
||||||
|
parserOptions: {
|
||||||
|
parser: 'babel-eslint',
|
||||||
|
},
|
||||||
|
globals: {
|
||||||
|
...globals.browser,
|
||||||
|
...globals.node,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
plugins: {
|
||||||
|
html,
|
||||||
|
},
|
||||||
|
|
||||||
|
extends: compat.extends('plugin:prettier/recommended'),
|
||||||
|
|
||||||
|
rules: {
|
||||||
|
'prettier/prettier': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
endOfLine: 'auto',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
indent: [
|
||||||
|
'error',
|
||||||
|
2,
|
||||||
|
{
|
||||||
|
SwitchCase: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
'space-infix-ops': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
int32Hint: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
'key-spacing': [
|
||||||
|
2,
|
||||||
|
{
|
||||||
|
beforeColon: false,
|
||||||
|
afterColon: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
'no-octal': 2,
|
||||||
|
'no-redeclare': 2,
|
||||||
|
'comma-spacing': 2,
|
||||||
|
'no-new-object': 2,
|
||||||
|
'arrow-spacing': 2,
|
||||||
|
|
||||||
|
quotes: [
|
||||||
|
2,
|
||||||
|
'single',
|
||||||
|
{
|
||||||
|
avoidEscape: true,
|
||||||
|
allowTemplateLiterals: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
globalIgnores(['**/public/']),
|
||||||
|
{
|
||||||
|
files: ['**/*.ts'],
|
||||||
|
languageOptions: {
|
||||||
|
parser: tsParser,
|
||||||
|
},
|
||||||
|
extends: compat.extends('plugin:@typescript-eslint/recommended'),
|
||||||
|
},
|
||||||
|
])
|
||||||
@@ -2,6 +2,7 @@ const fs = require('fs')
|
|||||||
const path = require('path')
|
const path = require('path')
|
||||||
const { register_anonimous } = require('./main')
|
const { register_anonimous } = require('./main')
|
||||||
const { cookieToJson, generateRandomChineseIP } = require('./util/index')
|
const { cookieToJson, generateRandomChineseIP } = require('./util/index')
|
||||||
|
const { getXeapiPublicKey } = require('./util/xeapiKey')
|
||||||
const tmpPath = require('os').tmpdir()
|
const tmpPath = require('os').tmpdir()
|
||||||
|
|
||||||
async function generateConfig() {
|
async function generateConfig() {
|
||||||
@@ -20,5 +21,21 @@ async function generateConfig() {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
console.log(error)
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
let currentPublicKey = {}
|
||||||
|
try {
|
||||||
|
currentPublicKey = JSON.parse(
|
||||||
|
fs.readFileSync(path.resolve(tmpPath, 'xeapi_public_key'), 'utf-8'),
|
||||||
|
)
|
||||||
|
} catch (_) {}
|
||||||
|
const publicKey = await getXeapiPublicKey(currentPublicKey, global.deviceId)
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.resolve(tmpPath, 'xeapi_public_key'),
|
||||||
|
JSON.stringify(publicKey),
|
||||||
|
'utf-8',
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
module.exports = generateConfig
|
module.exports = generateConfig
|
||||||
|
|||||||
Vendored
+731
@@ -2,6 +2,7 @@ export interface RequestBaseConfig {
|
|||||||
cookie?: string
|
cookie?: string
|
||||||
realIP?: string // IPv4/IPv6 filled in X-Real-IP
|
realIP?: string // IPv4/IPv6 filled in X-Real-IP
|
||||||
proxy?: string // HTTP proxy
|
proxy?: string // HTTP proxy
|
||||||
|
randomCNIP?: boolean // Whether to use a random Chinese IP address filled in X-Real-IP
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MultiPageConfig {
|
export interface MultiPageConfig {
|
||||||
@@ -1165,6 +1166,13 @@ export function song_url_v1(
|
|||||||
params: { id: string | number; level: SoundQualityType } & RequestBaseConfig,
|
params: { id: string | number; level: SoundQualityType } & RequestBaseConfig,
|
||||||
): Promise<Response>
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function song_copyright_rcmd(
|
||||||
|
params: {
|
||||||
|
songid?: string | number
|
||||||
|
id?: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
export function top_album(
|
export function top_album(
|
||||||
params: {
|
params: {
|
||||||
area?: AlbumListArea
|
area?: AlbumListArea
|
||||||
@@ -1714,6 +1722,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
|
||||||
@@ -1831,3 +1841,724 @@ export function voice_lyric(
|
|||||||
id: number | string
|
id: number | string
|
||||||
} & RequestBaseConfig,
|
} & RequestBaseConfig,
|
||||||
): Promise<Response>
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function aidj_content_rcmd(
|
||||||
|
params: {
|
||||||
|
latitude?: string | number
|
||||||
|
longitude?: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function album_privilege(
|
||||||
|
params: {
|
||||||
|
id: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function artist_detail_dynamic(
|
||||||
|
params: {
|
||||||
|
id: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function artist_follow_count(
|
||||||
|
params: {
|
||||||
|
id: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function broadcast_category_region_get(
|
||||||
|
params: RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function broadcast_channel_collect_list(
|
||||||
|
params: MultiPageConfig & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function broadcast_channel_currentinfo(
|
||||||
|
params: {
|
||||||
|
id: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function broadcast_channel_list(
|
||||||
|
params: {
|
||||||
|
categoryId?: string | number
|
||||||
|
regionId?: string | number
|
||||||
|
lastId?: string | number
|
||||||
|
score?: string | number
|
||||||
|
} & MultiPageConfig &
|
||||||
|
RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function broadcast_sub(
|
||||||
|
params: {
|
||||||
|
t: SubAction
|
||||||
|
id: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function chart_detail(
|
||||||
|
params: {
|
||||||
|
chartCode: string | number
|
||||||
|
targetId: string | number
|
||||||
|
targetType: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function chart_song_detail(
|
||||||
|
params: {
|
||||||
|
chartCode: string | number
|
||||||
|
targetId: string | number
|
||||||
|
targetType: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function cloud_import(
|
||||||
|
params: {
|
||||||
|
md5: string
|
||||||
|
id?: string | number
|
||||||
|
bitrate?: string | number
|
||||||
|
fileSize?: string | number
|
||||||
|
artist?: string
|
||||||
|
album?: string
|
||||||
|
song?: string
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function cloud_lyric_get(
|
||||||
|
params: {
|
||||||
|
uid: string | number
|
||||||
|
sid: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function cloud_upload_complete(
|
||||||
|
params: {
|
||||||
|
songId: string | number
|
||||||
|
resourceId: string | number
|
||||||
|
md5: string
|
||||||
|
filename: string
|
||||||
|
song?: string
|
||||||
|
artist?: string
|
||||||
|
album?: string
|
||||||
|
bitrate?: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function cloud_upload_token(
|
||||||
|
params: {
|
||||||
|
md5: string
|
||||||
|
fileSize: string | number
|
||||||
|
filename: string
|
||||||
|
bitrate?: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function comment_info_list(
|
||||||
|
params: {
|
||||||
|
ids?: string
|
||||||
|
type?: CommentType
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function comment_report(
|
||||||
|
params: {
|
||||||
|
id: string | number
|
||||||
|
cid: string | number
|
||||||
|
reason: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function creator_authinfo_get(
|
||||||
|
params: RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function dj_difm_all_style_channel(
|
||||||
|
params: {
|
||||||
|
sources?: string
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function dj_difm_channel_subscribe(
|
||||||
|
params: {
|
||||||
|
id: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function dj_difm_channel_unsubscribe(
|
||||||
|
params: {
|
||||||
|
id: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function dj_difm_playing_tracks_list(
|
||||||
|
params: {
|
||||||
|
channelId: string | number
|
||||||
|
limit?: string | number
|
||||||
|
source?: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function dj_difm_subscribe_channels_get(
|
||||||
|
params: {
|
||||||
|
sources?: string
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function fanscenter_basicinfo_age_get(
|
||||||
|
params: RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function fanscenter_basicinfo_gender_get(
|
||||||
|
params: RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function fanscenter_basicinfo_province_get(
|
||||||
|
params: RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function fanscenter_overview_get(
|
||||||
|
params: RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function fanscenter_trend_list(
|
||||||
|
params: {
|
||||||
|
startTime?: string | number
|
||||||
|
endTime?: string | number
|
||||||
|
type?: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function lbs_city_code(
|
||||||
|
params: {
|
||||||
|
bizCode?: string
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function listen_data_realtime_report(
|
||||||
|
params: {
|
||||||
|
type?: string
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function listen_data_report(
|
||||||
|
params: {
|
||||||
|
type?: string
|
||||||
|
endTime?: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function listen_data_today_song(
|
||||||
|
params: RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function listen_data_total(params: RequestBaseConfig): Promise<Response>
|
||||||
|
|
||||||
|
export function listen_data_year_report(
|
||||||
|
params: RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function listentogether_accept(
|
||||||
|
params: {
|
||||||
|
roomId: string | number
|
||||||
|
inviterId: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function listentogether_end(
|
||||||
|
params: {
|
||||||
|
roomId: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function listentogether_heatbeat(
|
||||||
|
params: {
|
||||||
|
roomId: string | number
|
||||||
|
songId: string | number
|
||||||
|
playStatus: string | number
|
||||||
|
progress: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function listentogether_play_command(
|
||||||
|
params: {
|
||||||
|
roomId: string | number
|
||||||
|
commandType: string | number
|
||||||
|
progress?: string | number
|
||||||
|
playStatus: string | number
|
||||||
|
formerSongId: string | number
|
||||||
|
targetSongId: string | number
|
||||||
|
clientSeq: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function listentogether_room_check(
|
||||||
|
params: {
|
||||||
|
roomId: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function listentogether_room_create(
|
||||||
|
params: RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function listentogether_sync_list_command(
|
||||||
|
params: {
|
||||||
|
roomId: string | number
|
||||||
|
commandType: string | number
|
||||||
|
userId: string | number
|
||||||
|
version: string | number
|
||||||
|
randomList: string
|
||||||
|
displayList: string
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function listentogether_sync_playlist_get(
|
||||||
|
params: {
|
||||||
|
roomId: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function mlog_music_rcmd(
|
||||||
|
params: {
|
||||||
|
mvid?: string | number
|
||||||
|
songid?: string | number
|
||||||
|
limit?: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function music_first_listen_info(
|
||||||
|
params: {
|
||||||
|
id: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function personal_fm_mode(
|
||||||
|
params: {
|
||||||
|
mode: string
|
||||||
|
submode?: string
|
||||||
|
limit?: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function playlist_category_list(
|
||||||
|
params: {
|
||||||
|
cat?: string
|
||||||
|
limit?: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function playlist_detail_rcmd_get(
|
||||||
|
params: {
|
||||||
|
id: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function playlist_import_name_task_create(
|
||||||
|
params: {
|
||||||
|
local?: string
|
||||||
|
importStarPlaylist?: boolean
|
||||||
|
['name']?: string
|
||||||
|
['description']?: string
|
||||||
|
['id']?: string | number
|
||||||
|
['url']?: string
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function playlist_import_task_status(
|
||||||
|
params: {
|
||||||
|
id: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function playlist_privacy(
|
||||||
|
params: {
|
||||||
|
id: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function playmode_song_vector(
|
||||||
|
params: {
|
||||||
|
ids: string
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function radio_sport_get(
|
||||||
|
params: {
|
||||||
|
bpm?: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function recent_listen_list(params: RequestBaseConfig): Promise<Response>
|
||||||
|
|
||||||
|
export function recommend_songs_dislike(
|
||||||
|
params: {
|
||||||
|
id: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function register_anonimous(params: RequestBaseConfig): Promise<Response>
|
||||||
|
|
||||||
|
export function sati_resource_list(
|
||||||
|
params: {
|
||||||
|
tag: string
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function sati_resource_list_more(
|
||||||
|
params: {
|
||||||
|
id: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function sati_resource_sub(
|
||||||
|
params: {
|
||||||
|
id: string | number
|
||||||
|
cancel?: string | boolean
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function sati_resource_sub_list(
|
||||||
|
params: RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function sati_tag_list(params: RequestBaseConfig): Promise<Response>
|
||||||
|
|
||||||
|
export function sati_timescene_resources_get(
|
||||||
|
params: RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function search_match(
|
||||||
|
params: {
|
||||||
|
title?: string
|
||||||
|
album?: string
|
||||||
|
artist?: string
|
||||||
|
duration?: string | number
|
||||||
|
md5?: string
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function search_suggest_pc(
|
||||||
|
params: {
|
||||||
|
keyword: string
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function send_album(
|
||||||
|
params: {
|
||||||
|
id: string | number
|
||||||
|
msg?: string
|
||||||
|
user_ids: string
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function send_song(
|
||||||
|
params: {
|
||||||
|
id: string | number
|
||||||
|
msg?: string
|
||||||
|
user_ids: string
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function song_chorus(
|
||||||
|
params: {
|
||||||
|
id: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function song_creators(
|
||||||
|
params: {
|
||||||
|
id: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function song_downlist(
|
||||||
|
params: MultiPageConfig & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function song_download_url_v1(
|
||||||
|
params: {
|
||||||
|
id: string | number
|
||||||
|
level: SoundQualityType
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function song_dynamic_cover(
|
||||||
|
params: {
|
||||||
|
id: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function song_like(
|
||||||
|
params: {
|
||||||
|
id: string | number
|
||||||
|
like?: string | boolean
|
||||||
|
uid?: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function song_like_check(
|
||||||
|
params: {
|
||||||
|
ids: string
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function song_lyrics_mark(
|
||||||
|
params: {
|
||||||
|
id: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function song_lyrics_mark_add(
|
||||||
|
params: {
|
||||||
|
id: string | number
|
||||||
|
markId?: string
|
||||||
|
data?: string
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function song_lyrics_mark_del(
|
||||||
|
params: {
|
||||||
|
id: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function song_lyrics_mark_user_page(
|
||||||
|
params: MultiPageConfig & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function song_monthdownlist(
|
||||||
|
params: MultiPageConfig & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function song_music_detail(
|
||||||
|
params: {
|
||||||
|
id: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function song_red_count(
|
||||||
|
params: {
|
||||||
|
id: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function song_singledownlist(
|
||||||
|
params: MultiPageConfig & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function song_url_match(
|
||||||
|
params: {
|
||||||
|
id: string | number
|
||||||
|
source?: string
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function song_url_ncmget(params: RequestBaseConfig): Promise<Response>
|
||||||
|
|
||||||
|
export function song_url_v1_302(
|
||||||
|
params: {
|
||||||
|
id: string | number
|
||||||
|
level: SoundQualityType
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function starpick_comments_summary(
|
||||||
|
params: RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function summary_annual(
|
||||||
|
params: {
|
||||||
|
year: string
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function threshold_detail_get(
|
||||||
|
params: RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function toplist_detail_v2(params: RequestBaseConfig): Promise<Response>
|
||||||
|
|
||||||
|
export function ugc_album_get(
|
||||||
|
params: {
|
||||||
|
id: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function ugc_artist_get(
|
||||||
|
params: {
|
||||||
|
id: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function ugc_artist_search(
|
||||||
|
params: {
|
||||||
|
keyword: string
|
||||||
|
limit?: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function ugc_detail(
|
||||||
|
params: {
|
||||||
|
auditStatus?: string
|
||||||
|
type?: string | number
|
||||||
|
sortBy?: string
|
||||||
|
order?: string
|
||||||
|
} & MultiPageConfig &
|
||||||
|
RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function ugc_mv_get(
|
||||||
|
params: {
|
||||||
|
id: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function ugc_song_get(
|
||||||
|
params: {
|
||||||
|
id: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function ugc_user_devote(params: RequestBaseConfig): Promise<Response>
|
||||||
|
|
||||||
|
export function user_detail_new(
|
||||||
|
params: {
|
||||||
|
uid: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function user_follow_mixed(
|
||||||
|
params: {
|
||||||
|
size?: string | number
|
||||||
|
cursor?: string | number
|
||||||
|
scene?: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function user_medal(
|
||||||
|
params: {
|
||||||
|
uid: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function user_mutualfollow_get(
|
||||||
|
params: {
|
||||||
|
uid: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function user_playlist_collect(
|
||||||
|
params: {
|
||||||
|
uid: string | number
|
||||||
|
} & MultiPageConfig &
|
||||||
|
RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function user_playlist_create(
|
||||||
|
params: {
|
||||||
|
uid: string | number
|
||||||
|
} & MultiPageConfig &
|
||||||
|
RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function user_social_status(
|
||||||
|
params: {
|
||||||
|
uid: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function user_social_status_edit(
|
||||||
|
params: {
|
||||||
|
type: string | number
|
||||||
|
iconUrl?: string
|
||||||
|
content?: string
|
||||||
|
actionUrl?: string
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function user_social_status_rcmd(
|
||||||
|
params: RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function user_social_status_support(
|
||||||
|
params: RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function verify_getQr(
|
||||||
|
params: {
|
||||||
|
vid: string | number
|
||||||
|
type: string | number
|
||||||
|
token: string
|
||||||
|
evid: string
|
||||||
|
sign: string
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function verify_qrcodestatus(
|
||||||
|
params: {
|
||||||
|
qr: string
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function vip_sign(params: RequestBaseConfig): Promise<Response>
|
||||||
|
|
||||||
|
export function vip_sign_info(params: RequestBaseConfig): Promise<Response>
|
||||||
|
|
||||||
|
export function vip_tasks_v1(
|
||||||
|
params: {
|
||||||
|
id?: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function voice_detail(
|
||||||
|
params: {
|
||||||
|
id: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function voice_upload(
|
||||||
|
params: {
|
||||||
|
songFile: {
|
||||||
|
name: string
|
||||||
|
data: string | Buffer
|
||||||
|
}
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function voicelist_detail(
|
||||||
|
params: {
|
||||||
|
id: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function voicelist_list(
|
||||||
|
params: {
|
||||||
|
voiceListId: string | number
|
||||||
|
} & MultiPageConfig &
|
||||||
|
RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function voicelist_my_created(
|
||||||
|
params: {
|
||||||
|
limit?: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function voicelist_search(
|
||||||
|
params: {
|
||||||
|
keyword?: string
|
||||||
|
limit?: string | number
|
||||||
|
offset?: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function voicelist_trans(
|
||||||
|
params: {
|
||||||
|
radioId?: string | number
|
||||||
|
programId?: string | number
|
||||||
|
position?: string | number
|
||||||
|
} & MultiPageConfig &
|
||||||
|
RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
## 环境
|
|
||||||
- 系统/平台: <你的系统和平台>
|
|
||||||
|
|
||||||
- nodejs 版本: <你的 NodeJS 版本号>
|
|
||||||
|
|
||||||
- API版本:<运行的云音乐 API 的版本号, 对应 package.json 里面的 version>
|
|
||||||
|
|
||||||
## 出现问题
|
|
||||||
<出现的问题>
|
|
||||||
|
|
||||||
## 重现步骤
|
|
||||||
<重现步骤>
|
|
||||||
|
|
||||||
## 期待效果
|
|
||||||
<现在的效果,期待的效果>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
>先看文档有没有相关说明,调用前须知必看
|
|
||||||
|
|
||||||
>先在 issues 搜一下是否有相似问题,没有再发,否则直接关闭
|
|
||||||
|
|
||||||
>不处理别人搭建的线上服务的问题,此项目不提供任何线上服务,请自行搭建
|
|
||||||
|
|
||||||
>重现步骤尽量详细,不能含糊不清,包含请求地址和对应参数以及操作过程描述,不是每个人都喜欢猜别人遇到了什么问题和找参数一个个试,也比较浪费时间
|
|
||||||
|
|
||||||
>如果不是提建议,提 issues 如果不照着模版来将不会优先处理或直接关闭
|
|
||||||
|
|
||||||
>460 cheating 的问题把 `util/request.js` 里面的 `headers['X-Real-IP']` 的注释取消掉就好
|
|
||||||
@@ -3,47 +3,61 @@ const path = require('path')
|
|||||||
const tmpPath = require('os').tmpdir()
|
const tmpPath = require('os').tmpdir()
|
||||||
const { cookieToJson } = require('./util')
|
const { cookieToJson } = require('./util')
|
||||||
|
|
||||||
if (!fs.existsSync(path.resolve(tmpPath, 'anonymous_token'))) {
|
const anonymousTokenPath = path.resolve(tmpPath, 'anonymous_token')
|
||||||
fs.writeFileSync(path.resolve(tmpPath, 'anonymous_token'), '', 'utf-8')
|
if (!fs.existsSync(anonymousTokenPath)) {
|
||||||
|
fs.writeFileSync(anonymousTokenPath, '', 'utf-8')
|
||||||
}
|
}
|
||||||
|
|
||||||
let firstRun = true
|
|
||||||
/** @type {Record<string, any>} */
|
/** @type {Record<string, any>} */
|
||||||
let obj = {}
|
let obj = {}
|
||||||
fs.readdirSync(path.join(__dirname, 'module'))
|
|
||||||
.reverse()
|
|
||||||
.forEach((file) => {
|
|
||||||
if (!file.endsWith('.js')) return
|
|
||||||
let fileModule = require(path.join(__dirname, 'module', file))
|
|
||||||
let fn = file.split('.').shift() || ''
|
|
||||||
obj[fn] = function (data = {}) {
|
|
||||||
if (typeof data.cookie === 'string') {
|
|
||||||
data.cookie = cookieToJson(data.cookie)
|
|
||||||
}
|
|
||||||
return fileModule(
|
|
||||||
{
|
|
||||||
...data,
|
|
||||||
cookie: data.cookie ? data.cookie : {},
|
|
||||||
},
|
|
||||||
async (...args) => {
|
|
||||||
if (firstRun) {
|
|
||||||
firstRun = false
|
|
||||||
const generateConfig = require('./generateConfig')
|
|
||||||
await generateConfig()
|
|
||||||
}
|
|
||||||
// 待优化
|
|
||||||
const request = require('./util/request')
|
|
||||||
|
|
||||||
return request(...args)
|
const modulePath = path.join(__dirname, 'module')
|
||||||
},
|
const moduleFiles = fs.readdirSync(modulePath).reverse()
|
||||||
)
|
|
||||||
}
|
let requestModule = null
|
||||||
})
|
|
||||||
|
moduleFiles.forEach((file) => {
|
||||||
|
if (!file.endsWith('.js')) return
|
||||||
|
|
||||||
|
const filePath = path.join(modulePath, file)
|
||||||
|
let fileModule = require(filePath)
|
||||||
|
let fn = file.split('.').shift() || ''
|
||||||
|
|
||||||
|
obj[fn] = function (data = {}) {
|
||||||
|
const cookie =
|
||||||
|
typeof data.cookie === 'string'
|
||||||
|
? cookieToJson(data.cookie)
|
||||||
|
: data.cookie || {}
|
||||||
|
|
||||||
|
return fileModule(
|
||||||
|
{
|
||||||
|
...data,
|
||||||
|
cookie,
|
||||||
|
},
|
||||||
|
async (...args) => {
|
||||||
|
if (!requestModule) {
|
||||||
|
requestModule = require('./util/request')
|
||||||
|
}
|
||||||
|
|
||||||
|
return requestModule(...args)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
let serverModule = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {Record<string, any> & import("./server")}
|
* @type {Record<string, any> & import("./server")}
|
||||||
*/
|
*/
|
||||||
module.exports = {
|
module.exports = {
|
||||||
...require('./server'),
|
get server() {
|
||||||
|
if (!serverModule) {
|
||||||
|
serverModule = require('./server')
|
||||||
|
}
|
||||||
|
return serverModule
|
||||||
|
},
|
||||||
...obj,
|
...obj,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Object.assign(module.exports, require('./server'))
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ const createOption = require('../util/option.js')
|
|||||||
module.exports = (query, request) => {
|
module.exports = (query, request) => {
|
||||||
const data = {
|
const data = {
|
||||||
ctcode: query.ctcode || '86',
|
ctcode: query.ctcode || '86',
|
||||||
|
secrete: 'music_middleuser_pclogin',
|
||||||
cellphone: query.phone,
|
cellphone: query.phone,
|
||||||
}
|
}
|
||||||
return request(`/api/sms/captcha/sent`, data, createOption(query, 'weapi'))
|
return request(`/api/sms/captcha/sent`, data, createOption(query, 'weapi'))
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// 获取指定维度音乐排行榜详情
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
chartCode: query.chartCode,
|
||||||
|
targetId: query.targetId,
|
||||||
|
targetType: query.targetType,
|
||||||
|
}
|
||||||
|
return request(`/api/chart/detail`, data, createOption(query))
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// 获取指定维度音乐排行榜列表
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
chartCode: query.chartCode,
|
||||||
|
targetId: query.targetId,
|
||||||
|
targetType: query.targetType,
|
||||||
|
}
|
||||||
|
return request(`/api/chart/song/detail`, data, createOption(query))
|
||||||
|
}
|
||||||
+141
-123
@@ -1,24 +1,26 @@
|
|||||||
const mm = require('music-metadata')
|
|
||||||
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
|
||||||
module.exports = async (query, request) => {
|
module.exports = async (query, request) => {
|
||||||
let ext = 'mp3'
|
mm = require('music-metadata')
|
||||||
// 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,
|
||||||
@@ -28,119 +30,135 @@ 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)
|
||||||
}
|
|
||||||
const res = await request(
|
query.songFile.md5 = fileMd5
|
||||||
`/api/cloud/upload/check`,
|
query.songFile.size = fileSize
|
||||||
{
|
|
||||||
bitrate: String(bitrate),
|
|
||||||
ext: '',
|
|
||||||
length: query.songFile.size,
|
|
||||||
md5: query.songFile.md5,
|
|
||||||
songId: '0',
|
|
||||||
version: 1,
|
|
||||||
},
|
|
||||||
createOption(query),
|
|
||||||
)
|
|
||||||
let artist = ''
|
|
||||||
let album = ''
|
|
||||||
let songName = ''
|
|
||||||
try {
|
try {
|
||||||
const metadata = await mm.parseBuffer(
|
const res = await request(
|
||||||
query.songFile.data,
|
`/api/cloud/upload/check`,
|
||||||
query.songFile.mimetype,
|
{
|
||||||
|
bitrate: String(bitrate),
|
||||||
|
ext: '',
|
||||||
|
length: fileSize,
|
||||||
|
md5: fileMd5,
|
||||||
|
songId: '0',
|
||||||
|
version: 1,
|
||||||
|
},
|
||||||
|
createOption(query),
|
||||||
)
|
)
|
||||||
const info = metadata.common
|
|
||||||
|
|
||||||
if (info.title) {
|
let artist = ''
|
||||||
songName = info.title
|
let album = ''
|
||||||
}
|
let songName = ''
|
||||||
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) {
|
|
||||||
logger.info(error)
|
|
||||||
}
|
|
||||||
const tokenRes = await request(
|
|
||||||
`/api/nos/token/alloc`,
|
|
||||||
{
|
|
||||||
bucket: '',
|
|
||||||
ext: ext,
|
|
||||||
filename: filename,
|
|
||||||
local: false,
|
|
||||||
nos_product: 3,
|
|
||||||
type: 'audio',
|
|
||||||
md5: query.songFile.md5,
|
|
||||||
},
|
|
||||||
createOption(query),
|
|
||||||
)
|
|
||||||
|
|
||||||
if (res.body.needUpload) {
|
try {
|
||||||
const uploadInfo = await uploadPlugin(query, request)
|
let metadata
|
||||||
// logger.info('uploadInfo', uploadInfo.body.result.resourceId)
|
if (useTemp) {
|
||||||
}
|
metadata = await mm.parseFile(query.songFile.tempFilePath)
|
||||||
// logger.info(tokenRes.body.result)
|
} else {
|
||||||
const res2 = await request(
|
metadata = await mm.parseBuffer(
|
||||||
`/api/upload/cloud/info/v2`,
|
query.songFile.data,
|
||||||
{
|
query.songFile.mimetype,
|
||||||
md5: query.songFile.md5,
|
)
|
||||||
songid: res.body.songId,
|
}
|
||||||
filename: query.songFile.name,
|
const info = metadata.common
|
||||||
song: songName || filename,
|
if (info.title) songName = info.title
|
||||||
album: album || '未知专辑',
|
if (info.album) album = info.album
|
||||||
artist: artist || '未知艺术家',
|
if (info.artist) artist = info.artist
|
||||||
bitrate: String(bitrate),
|
} catch (error) {
|
||||||
resourceId: tokenRes.body.result.resourceId,
|
logger.info('元数据解析错误:', error.message)
|
||||||
},
|
}
|
||||||
createOption(query),
|
|
||||||
)
|
const tokenRes = await request(
|
||||||
// logger.info({ res2, privateCloud: res2.body.privateCloud })
|
`/api/nos/token/alloc`,
|
||||||
// logger.info(res.body.songId, 'songid')
|
{
|
||||||
const res3 = await request(
|
bucket: '',
|
||||||
`/api/cloud/pub/v2`,
|
ext: ext,
|
||||||
{
|
filename: filename,
|
||||||
songid: res2.body.songId,
|
local: false,
|
||||||
},
|
nos_product: 3,
|
||||||
createOption(query),
|
type: 'audio',
|
||||||
)
|
md5: fileMd5,
|
||||||
// logger.info({ res3 })
|
},
|
||||||
return {
|
createOption(query),
|
||||||
status: 200,
|
)
|
||||||
body: {
|
|
||||||
...res.body,
|
if (!tokenRes.body.result || !tokenRes.body.result.resourceId) {
|
||||||
...res3.body,
|
logger.error('Token分配失败:', tokenRes.body)
|
||||||
// ...uploadInfo,
|
return Promise.reject({
|
||||||
},
|
status: 500,
|
||||||
cookie: res.cookie,
|
body: {
|
||||||
|
code: 500,
|
||||||
|
msg: '获取上传token失败',
|
||||||
|
detail: tokenRes.body,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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(
|
||||||
|
`/api/upload/cloud/info/v2`,
|
||||||
|
{
|
||||||
|
md5: fileMd5,
|
||||||
|
songid: res.body.songId,
|
||||||
|
filename: query.songFile.name,
|
||||||
|
song: songName || filename,
|
||||||
|
album: album || '未知专辑',
|
||||||
|
artist: artist || '未知艺术家',
|
||||||
|
bitrate: String(bitrate),
|
||||||
|
resourceId: tokenRes.body.result.resourceId,
|
||||||
|
},
|
||||||
|
createOption(query),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (res2.body.code !== 200) {
|
||||||
|
logger.error('云盘信息上传失败:', res2.body)
|
||||||
|
return Promise.reject({
|
||||||
|
status: res2.status || 500,
|
||||||
|
body: {
|
||||||
|
code: res2.body.code || 500,
|
||||||
|
msg: res2.body.msg || '上传云盘信息失败',
|
||||||
|
detail: res2.body,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const res3 = await request(
|
||||||
|
`/api/cloud/pub/v2`,
|
||||||
|
{
|
||||||
|
songid: res2.body.songId,
|
||||||
|
},
|
||||||
|
createOption(query),
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
...res.body,
|
||||||
|
...res3.body,
|
||||||
|
},
|
||||||
|
cookie: res.cookie,
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (useTemp) {
|
||||||
|
await cleanupTempFile(query.songFile.tempFilePath)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// 获取云盘歌词
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
userId: query.uid,
|
||||||
|
songId: query.sid,
|
||||||
|
lv: -1,
|
||||||
|
kv: -1,
|
||||||
|
}
|
||||||
|
return request(`/api/cloud/lyric/get`, data, createOption(query, 'eapi'))
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
// 评论统计数据
|
||||||
|
// type: 0=歌曲 1=MV 2=歌单 3=专辑 4=电台节目 5=视频 6=动态 7=电台
|
||||||
|
// ids: 资源 ID 列表,多个用逗号分隔,如 "123,456"
|
||||||
|
const { resourceTypeMap } = require('../util/config.json')
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
|
||||||
|
// 从 resourceTypeMap 的前缀值中提取网易云内部资源类型编号
|
||||||
|
// 例如 "R_SO_4_" -> "4", "A_DR_14_" -> "14"
|
||||||
|
const resourceTypeIdMap = Object.fromEntries(
|
||||||
|
Object.entries(resourceTypeMap).map(([key, prefix]) => [
|
||||||
|
key,
|
||||||
|
prefix.replace(/_$/, '').split('_').pop(),
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const ids = String(query.ids || query.id || '')
|
||||||
|
.split(',')
|
||||||
|
.map((id) => id.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
|
||||||
|
return request(
|
||||||
|
`/api/resource/commentInfo/list`,
|
||||||
|
{
|
||||||
|
resourceType: resourceTypeIdMap[String(query.type || 0)],
|
||||||
|
resourceIds: JSON.stringify(ids),
|
||||||
|
},
|
||||||
|
createOption(query, 'weapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// 举报评论
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
threadId: 'R_SO_4_' + query.id,
|
||||||
|
commentId: query.cid,
|
||||||
|
reason: query.reason,
|
||||||
|
}
|
||||||
|
return request(`/api/report/reportcomment`, data, createOption(query))
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
const {
|
||||||
|
eapiResDecrypt,
|
||||||
|
eapiReqDecrypt,
|
||||||
|
aesDecrypt,
|
||||||
|
xeapiResDecrypt,
|
||||||
|
} = require('../util/crypto')
|
||||||
|
const CryptoJS = require('crypto-js')
|
||||||
|
|
||||||
|
const linuxapiKey = 'rFgB&h#%2?^eDg:Q'
|
||||||
|
|
||||||
|
module.exports = async (query, request) => {
|
||||||
|
const crypto = query.crypto || 'eapi'
|
||||||
|
const data = query.data || query.hexString || ''
|
||||||
|
const isReq = query.isReq !== 'false'
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return {
|
||||||
|
status: 400,
|
||||||
|
body: { code: 400, message: 'data is required' },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let result
|
||||||
|
switch (crypto) {
|
||||||
|
case 'eapi': {
|
||||||
|
const pureHex = data.replace(/\s/g, '')
|
||||||
|
result = isReq ? eapiReqDecrypt(pureHex) : eapiResDecrypt(pureHex)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'weapi': {
|
||||||
|
if (isReq) {
|
||||||
|
return {
|
||||||
|
status: 400,
|
||||||
|
body: {
|
||||||
|
code: 400,
|
||||||
|
message:
|
||||||
|
'weapi 请求解密需要 RSA 私钥,暂不支持;仅支持 weapi 返回数据解密(e_r=true 时与 eapi 相同)',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const pureHex = data.replace(/\s/g, '')
|
||||||
|
result = eapiResDecrypt(pureHex)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'linuxapi': {
|
||||||
|
if (isReq) {
|
||||||
|
const pureHex = data.replace(/\s/g, '')
|
||||||
|
const decrypted = aesDecrypt(pureHex, linuxapiKey, '', 'hex')
|
||||||
|
result = JSON.parse(decrypted.toString(CryptoJS.enc.Utf8))
|
||||||
|
} else {
|
||||||
|
result = typeof data === 'string' ? JSON.parse(data) : data
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'xeapi': {
|
||||||
|
if (isReq) {
|
||||||
|
return {
|
||||||
|
status: 400,
|
||||||
|
body: {
|
||||||
|
code: 400,
|
||||||
|
message:
|
||||||
|
'xeapi 请求解密涉及 X25519 ECDH 密钥交换,流程复杂,暂不支持;仅支持 xeapi 返回数据解密',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const buf = Buffer.from(data, 'base64')
|
||||||
|
result = xeapiResDecrypt(buf)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'api': {
|
||||||
|
result = typeof data === 'string' ? JSON.parse(data) : data
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
status: 400,
|
||||||
|
body: { code: 400, message: `未知加密方式: ${crypto}` },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
body: { code: 200, data: result },
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
status: 400,
|
||||||
|
body: { code: 400, message: `解密失败: ${error.message}` },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
// DIFM电台 - 分类
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
sources: query.sources || '[0]',
|
||||||
|
}
|
||||||
|
return request(`/api/dj/difm/all/style/channel/v2`, data, createOption(query))
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
// DIFM电台 - 收藏频道
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
id: query.id,
|
||||||
|
}
|
||||||
|
return request(`/api/dj/difm/channel/subscribe`, data, createOption(query))
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
// DIFM电台 - 取消收藏频道
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
id: query.id,
|
||||||
|
}
|
||||||
|
return request(`/api/dj/difm/channel/unsubscribe`, data, createOption(query))
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// DIFM电台 - 播放列表
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
limit: query.limit || 5,
|
||||||
|
source: query.source || 0,
|
||||||
|
channelId: query.channelId,
|
||||||
|
}
|
||||||
|
return request(`/api/dj/difm/playing/tracks/list`, data, createOption(query))
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
// DIFM电台 - 收藏列表
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
sources: query.sources || '[0]',
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
`/api/dj/difm/subscribe/channels/get/v2`,
|
||||||
|
data,
|
||||||
|
createOption(query),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
// 多级行政区划数据获取接口
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
bizCode: query.bizCode || '',
|
||||||
|
}
|
||||||
|
return request(`/api/lbs/city/code`, data, createOption(query))
|
||||||
|
}
|
||||||
@@ -13,12 +13,12 @@ module.exports = async (query, request) => {
|
|||||||
[query.captcha ? 'captcha' : 'password']: query.captcha
|
[query.captcha ? 'captcha' : 'password']: query.captcha
|
||||||
? query.captcha
|
? query.captcha
|
||||||
: query.md5_password || CryptoJS.MD5(query.password).toString(),
|
: query.md5_password || CryptoJS.MD5(query.password).toString(),
|
||||||
rememberLogin: 'true',
|
remember: 'true',
|
||||||
}
|
}
|
||||||
let result = await request(
|
let result = await request(
|
||||||
`/api/w/login/cellphone`,
|
`/api/w/login/cellphone`,
|
||||||
data,
|
data,
|
||||||
createOption(query),
|
createOption(query, 'weapi'),
|
||||||
)
|
)
|
||||||
|
|
||||||
if (result.body.code === 200) {
|
if (result.body.code === 200) {
|
||||||
|
|||||||
@@ -1,9 +1,20 @@
|
|||||||
const QRCode = require('qrcode')
|
const QRCode = require('qrcode')
|
||||||
|
const { generateChainId } = require('../util/index')
|
||||||
|
|
||||||
const createOption = require('../util/option.js')
|
module.exports = (query) => {
|
||||||
module.exports = (query, request) => {
|
|
||||||
return new Promise(async (resolve) => {
|
return new Promise(async (resolve) => {
|
||||||
const url = `https://music.163.com/login?codekey=${query.key}`
|
const platform = query.platform || 'pc'
|
||||||
|
const cookie = query.cookie || ''
|
||||||
|
|
||||||
|
// 构建基础URL
|
||||||
|
let url = `https://music.163.com/login?codekey=${query.key}`
|
||||||
|
|
||||||
|
// 如果是web平台,则添加chainId参数
|
||||||
|
|
||||||
|
if (platform === 'web') {
|
||||||
|
const chainId = generateChainId(cookie)
|
||||||
|
url += `&chainId=${chainId}`
|
||||||
|
}
|
||||||
return resolve({
|
return resolve({
|
||||||
code: 200,
|
code: 200,
|
||||||
status: 200,
|
status: 200,
|
||||||
|
|||||||
@@ -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'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
// 私信和通知接口
|
// 私信和通知接口
|
||||||
|
|
||||||
const createOption = require('../util/option.js')
|
const createOption = require('../util/option.js')
|
||||||
module.exports = (query, request) => {
|
module.exports = (query, request) => {
|
||||||
const data = {}
|
const data = {}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// 歌单分类列表
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
cat: query.cat || '全部',
|
||||||
|
limit: query.limit || 24,
|
||||||
|
newStyle: true,
|
||||||
|
}
|
||||||
|
return request(`/api/playlist/category/list`, data, createOption(query))
|
||||||
|
}
|
||||||
@@ -2,5 +2,5 @@
|
|||||||
|
|
||||||
const createOption = require('../util/option.js')
|
const createOption = require('../util/option.js')
|
||||||
module.exports = (query, request) => {
|
module.exports = (query, request) => {
|
||||||
return request(`/api/playlist/catalogue`, {}, createOption(query, 'weapi'))
|
return request(`/api/playlist/catalogue`, {}, createOption(query, 'eapi'))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
// 收藏与取消收藏歌单
|
// 收藏与取消收藏歌单
|
||||||
|
const { APP_CONF } = require('../util/config.json')
|
||||||
const createOption = require('../util/option.js')
|
const createOption = require('../util/option.js')
|
||||||
module.exports = (query, request) => {
|
module.exports = (query, request) => {
|
||||||
query.t = query.t == 1 ? 'subscribe' : 'unsubscribe'
|
const path = query.t == 1 ? 'subscribe' : 'unsubscribe'
|
||||||
const data = {
|
const data = {
|
||||||
id: query.id,
|
id: query.id,
|
||||||
|
...(query.t === 1
|
||||||
|
? { checkToken: query.checkToken || APP_CONF.checkToken }
|
||||||
|
: {}),
|
||||||
}
|
}
|
||||||
return request(`/api/playlist/${query.t}`, data, createOption(query, 'weapi'))
|
query.checkToken = true // 强制开启checkToken
|
||||||
|
return request(`/api/playlist/${path}`, data, createOption(query, 'eapi'))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
// 跑步漫游
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
bpm: query.bpm || 50,
|
||||||
|
}
|
||||||
|
return request(`/api/radio/sport/get`, data, createOption(query))
|
||||||
|
}
|
||||||
@@ -2,7 +2,9 @@
|
|||||||
|
|
||||||
const createOption = require('../util/option.js')
|
const createOption = require('../util/option.js')
|
||||||
module.exports = (query, request) => {
|
module.exports = (query, request) => {
|
||||||
const data = {}
|
const data = {
|
||||||
|
afresh: query.afresh,
|
||||||
|
}
|
||||||
return request(
|
return request(
|
||||||
`/api/v3/discovery/recommend/songs`,
|
`/api/v3/discovery/recommend/songs`,
|
||||||
data,
|
data,
|
||||||
|
|||||||
@@ -1,18 +1,17 @@
|
|||||||
|
// 获取游客cookie
|
||||||
|
|
||||||
const CryptoJS = require('crypto-js')
|
const CryptoJS = require('crypto-js')
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
const ID_XOR_KEY_1 = '3go8&$8*3*3h0k(2)2'
|
const ID_XOR_KEY_1 = '3go8&$8*3*3h0k(2)2'
|
||||||
const deviceidText = fs.readFileSync(
|
const logger = require('../util/logger.js')
|
||||||
path.resolve(__dirname, '../data/deviceid.txt'),
|
|
||||||
'utf-8',
|
|
||||||
)
|
|
||||||
|
|
||||||
const createOption = require('../util/option.js')
|
const createOption = require('../util/option.js')
|
||||||
const deviceidList = deviceidText.split('\n')
|
const { generateDeviceId } = require('../util/index')
|
||||||
|
|
||||||
function getRandomFromList(list) {
|
// function getRandomFromList(list) {
|
||||||
return list[Math.floor(Math.random() * list.length)]
|
// return list[Math.floor(Math.random() * list.length)]
|
||||||
}
|
// }
|
||||||
function cloudmusic_dll_encode_id(some_id) {
|
function cloudmusic_dll_encode_id(some_id) {
|
||||||
let xoredString = ''
|
let xoredString = ''
|
||||||
for (let i = 0; i < some_id.length; i++) {
|
for (let i = 0; i < some_id.length; i++) {
|
||||||
@@ -26,7 +25,8 @@ function cloudmusic_dll_encode_id(some_id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
module.exports = async (query, request) => {
|
module.exports = async (query, request) => {
|
||||||
const deviceId = getRandomFromList(deviceidList)
|
const deviceId = generateDeviceId()
|
||||||
|
logger.info(`Successfully registered anonimous token, deviceId: ${deviceId}`)
|
||||||
global.deviceId = deviceId
|
global.deviceId = deviceId
|
||||||
const encodedId = CryptoJS.enc.Base64.stringify(
|
const encodedId = CryptoJS.enc.Base64.stringify(
|
||||||
CryptoJS.enc.Utf8.parse(
|
CryptoJS.enc.Utf8.parse(
|
||||||
@@ -39,7 +39,7 @@ module.exports = async (query, request) => {
|
|||||||
let result = await request(
|
let result = await request(
|
||||||
`/api/register/anonimous`,
|
`/api/register/anonimous`,
|
||||||
data,
|
data,
|
||||||
createOption(query, 'weapi'),
|
createOption(query, 'xeapi'),
|
||||||
)
|
)
|
||||||
if (result.body.code === 200) {
|
if (result.body.code === 200) {
|
||||||
result = {
|
result = {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ module.exports = (query, request) => {
|
|||||||
password: CryptoJS.MD5(query.password).toString(),
|
password: CryptoJS.MD5(query.password).toString(),
|
||||||
nickname: query.nickname,
|
nickname: query.nickname,
|
||||||
countrycode: query.countrycode || '86',
|
countrycode: query.countrycode || '86',
|
||||||
|
force: 'false',
|
||||||
}
|
}
|
||||||
return request(`/api/register/cellphone`, data, createOption(query))
|
return request(`/api/w/register/cellphone`, data, createOption(query))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
const { default: axios } = require('axios')
|
||||||
|
const encrypt = require('../util/crypto')
|
||||||
|
const { APP_CONF } = require('../util/config.json')
|
||||||
|
|
||||||
|
const generateNonce = () => {
|
||||||
|
let nonce = ''
|
||||||
|
for (let i = 0; i < 16; i++) {
|
||||||
|
nonce += Math.floor(Math.random() * 10).toString()
|
||||||
|
}
|
||||||
|
return nonce
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = async (query, request) => {
|
||||||
|
const nonce = generateNonce()
|
||||||
|
const timestamp = String(Date.now())
|
||||||
|
const deviceId = query.deviceId || global.deviceId || ''
|
||||||
|
const currentKeyVersion = query.currentKeyVersion || ''
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
appVersion: '9.1.65',
|
||||||
|
currentKeyVersion,
|
||||||
|
deviceId,
|
||||||
|
nonce,
|
||||||
|
os: 'android',
|
||||||
|
requestType: 'active',
|
||||||
|
signature: encrypt.xeapiSign(timestamp, nonce),
|
||||||
|
t1: '',
|
||||||
|
t2: '',
|
||||||
|
timestamp,
|
||||||
|
uid: '',
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await axios({
|
||||||
|
method: 'POST',
|
||||||
|
url: APP_CONF.apiDomain + '/api/gorilla/anti/crawler/security/key/get',
|
||||||
|
headers: {
|
||||||
|
'User-Agent':
|
||||||
|
'NeteaseMusic/9.1.65.240927161425(9001065);Dalvik/2.1.0 (Linux; U; Android 14; 23013RK75C Build/UKQ1.230804.001)',
|
||||||
|
Cookie: deviceId ? `deviceId=${encodeURIComponent(deviceId)}` : '',
|
||||||
|
},
|
||||||
|
data: new URLSearchParams(data).toString(),
|
||||||
|
proxy: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (
|
||||||
|
!res.data ||
|
||||||
|
res.data.code !== 200 ||
|
||||||
|
!res.data.data ||
|
||||||
|
!res.data.data.encryptedData
|
||||||
|
) {
|
||||||
|
throw new Error('xeapi public key request failed')
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!res.data.data.signature ||
|
||||||
|
encrypt.xeapiSign(res.data.data.timestamp, nonce) !==
|
||||||
|
res.data.data.signature
|
||||||
|
) {
|
||||||
|
throw new Error('xeapi public key response signature mismatch')
|
||||||
|
}
|
||||||
|
|
||||||
|
const publicKey = encrypt.xeapiDecryptPublicKey(res.data.data.encryptedData)
|
||||||
|
if (!publicKey.sk) {
|
||||||
|
throw new Error('xeapi public key response missing sk')
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
...publicKey,
|
||||||
|
deviceId,
|
||||||
|
},
|
||||||
|
cookie: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
// 提交歌曲播放状态
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
const generateSessionId = () =>
|
||||||
|
Array.from(
|
||||||
|
{ length: 12 },
|
||||||
|
() =>
|
||||||
|
'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'[Math.floor(Math.random() * 36)],
|
||||||
|
).join('')
|
||||||
|
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
sessionId,
|
||||||
|
progress = 0,
|
||||||
|
playMode = 'list_loop',
|
||||||
|
type = 'song',
|
||||||
|
} = query
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
return Promise.reject({
|
||||||
|
status: 400,
|
||||||
|
body: {
|
||||||
|
code: 400,
|
||||||
|
msg: '缺少必要参数:id',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const playStateSubmitReq = JSON.stringify({
|
||||||
|
resource: {
|
||||||
|
id: String(id),
|
||||||
|
type: type,
|
||||||
|
},
|
||||||
|
progress: Number(progress) || 0,
|
||||||
|
sessionId: sessionId || generateSessionId(),
|
||||||
|
playMode: playMode,
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
playStateSubmitReq: playStateSubmitReq,
|
||||||
|
}
|
||||||
|
|
||||||
|
return request(
|
||||||
|
'/api/relay/play/state/submit',
|
||||||
|
data,
|
||||||
|
createOption(query, 'weapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// 助眠解压 - 获取标签下资源列表
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
tag: query.tag,
|
||||||
|
firstQuery: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
return request(`/api/voice/sati/resource/list`, data, createOption(query))
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
// 助眠解压 - 查看同类推荐
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
id: query.id,
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
`/api/voice/sati/resource/list/more/v1`,
|
||||||
|
data,
|
||||||
|
createOption(query),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
// 助眠解压 - 收藏
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
id: query.id,
|
||||||
|
cancel: query.cancel || false,
|
||||||
|
}
|
||||||
|
return request(`/api/voice/sati/resource/sub`, data, createOption(query))
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
// 助眠解压 - 收藏列表
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {}
|
||||||
|
return request(`/api/voice/sati/resource/sub/list`, data, createOption(query))
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
// 助眠解压 - 标签列表
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {}
|
||||||
|
return request(`/api/voice/sati/tag/list`, data, createOption(query))
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
// 助眠解压 - 特定时间场景下的推荐资源
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
firstQuery: false,
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
`/api/voice/sati/timescene/resources/get`,
|
||||||
|
data,
|
||||||
|
createOption(query),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
// 搜索建议pc端
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
keyword: query.keyword || '',
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
`/api/search/pc/suggest/keyword/get`,
|
||||||
|
data,
|
||||||
|
createOption(query),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
// 从云盘获取歌曲下载链接
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
songId: query.id,
|
||||||
|
}
|
||||||
|
return request(`/api/cloud/dowonload`, data, createOption(query, 'eapi'))
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
// 灰色歌曲的其他版本推荐
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
songid: query.songid || query.id,
|
||||||
|
}
|
||||||
|
return request(`/api/song/copyright/rcmd`, data, createOption(query, 'eapi'))
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
// 歌曲创作者信息
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
songId: query.id,
|
||||||
|
}
|
||||||
|
return request(`/api/song/creators`, data, createOption(query))
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
// 喜欢歌曲
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const like = query.like !== 'false'
|
||||||
|
const data = {
|
||||||
|
trackId: query.id,
|
||||||
|
userid: query.uid,
|
||||||
|
like: like,
|
||||||
|
}
|
||||||
|
return request(`/api/song/like`, data, createOption(query))
|
||||||
|
}
|
||||||
+30
-27
@@ -5,31 +5,34 @@ const createOption = require('../util/option.js')
|
|||||||
const logger = require('../util/logger.js')
|
const logger = require('../util/logger.js')
|
||||||
|
|
||||||
module.exports = async (query, request) => {
|
module.exports = async (query, request) => {
|
||||||
try {
|
try {
|
||||||
const match = require("@unblockneteasemusic/server")
|
const {
|
||||||
const source = query.source
|
matchID,
|
||||||
? query.source.split(',') : ['pyncmd', 'kuwo', 'qq', 'migu', 'kugou']
|
} = require('@neteasecloudmusicapienhanced/unblockmusic-utils')
|
||||||
const server = query.server ? query.server.split(',') : query.server
|
const result = await matchID(query.id, query.source)
|
||||||
const result = await match(query.id, !server? source : server)
|
const proxy = process.env.PROXY_URL
|
||||||
const proxy = process.env.PROXY_URL;
|
logger.info('开始解灰', query.id, result)
|
||||||
logger.info("开始解灰", query.id, result)
|
const useProxy = process.env.ENABLE_PROXY || 'false'
|
||||||
const useProxy = process.env.ENABLE_PROXY || "false"
|
if (result.data.url && result.data.url.includes('kuwo')) {
|
||||||
if (result.url.includes('kuwo')) { result.proxyUrl = useProxy === 'true' ? proxy + result.url : result.url }
|
result.proxyUrl =
|
||||||
return {
|
useProxy === 'true' ? proxy + result.data.url : result.data.url
|
||||||
status: 200,
|
|
||||||
body: {
|
|
||||||
code: 200,
|
|
||||||
data: result,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
return {
|
|
||||||
status: 500,
|
|
||||||
body: {
|
|
||||||
code: 500,
|
|
||||||
msg: e.message || 'unblock error',
|
|
||||||
data: [],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
return {
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
code: 200,
|
||||||
|
data: result.data.url,
|
||||||
|
proxyUrl: result.proxyUrl || '',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return {
|
||||||
|
status: 500,
|
||||||
|
body: {
|
||||||
|
code: 500,
|
||||||
|
msg: e.message || 'unblock error',
|
||||||
|
data: [],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,75 +1,5 @@
|
|||||||
// GD音乐台get(适配SPlayer的UNM-Server)
|
// 夹带私货的东西就不要放在这里了
|
||||||
// 感谢来自GD Studio的开发API
|
|
||||||
// https://music.gdstudio.xyz/
|
|
||||||
|
|
||||||
const createOption = require('../util/option.js')
|
|
||||||
|
|
||||||
module.exports = async (query, request) => {
|
module.exports = async (query, request) => {
|
||||||
try {
|
return { status: 200, body: { code: 200, data: [] } }
|
||||||
const { id, br = "320" } = query;
|
|
||||||
if (!id) {
|
|
||||||
return {
|
|
||||||
status: 400,
|
|
||||||
body: {
|
|
||||||
code: 400,
|
|
||||||
message: "缺少必要参数 id",
|
|
||||||
data: [],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const validBR = ["128", "192", "320", "740", "999"];
|
|
||||||
// const covertBR = ["128000", "192000", "320000","740000", "999000"];
|
|
||||||
if (!validBR.includes(br)) {
|
|
||||||
return {
|
|
||||||
status: 400,
|
|
||||||
body: {
|
|
||||||
code: 400,
|
|
||||||
message: "无效音质参数",
|
|
||||||
allowed_values: validBR,
|
|
||||||
data: [],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const apiUrl = new URL("https://music-api.gdstudio.xyz/api.php");
|
|
||||||
apiUrl.searchParams.append("types", "url");
|
|
||||||
apiUrl.searchParams.append("id", id);
|
|
||||||
apiUrl.searchParams.append("br", br);
|
|
||||||
|
|
||||||
const response = await fetch(apiUrl.toString());
|
|
||||||
if (!response.ok) throw new Error(`API 响应状态: ${response.status}`);
|
|
||||||
const result = await response.json();
|
|
||||||
|
|
||||||
// 代理逻辑
|
|
||||||
const useProxy = process.env.ENABLE_PROXY || false;
|
|
||||||
const proxy = process.env.PROXY_URL;
|
|
||||||
if (useProxy && result.url && result.url.includes("kuwo")) {
|
|
||||||
result.proxyUrl = proxy + result.url.replace(/^http:\/\//, "http/");
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
status: 200,
|
|
||||||
body: {
|
|
||||||
code: 200,
|
|
||||||
message: "请求成功",
|
|
||||||
data: {
|
|
||||||
id,
|
|
||||||
br,
|
|
||||||
url: result.url,
|
|
||||||
...(proxy && result.proxyUrl ? { proxyUrl: result.proxyUrl } : {})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error in song_url_ncmget:", error);
|
|
||||||
return {
|
|
||||||
status: 500,
|
|
||||||
body: {
|
|
||||||
code: 500,
|
|
||||||
message: "服务器处理请求失败",
|
|
||||||
...(process.env.NODE_ENV === "development" ? { error: error.message } : {}),
|
|
||||||
data: [],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
// 网易云歌曲解灰
|
|
||||||
// 支持qq音乐、酷狗音乐、酷我音乐、咪咕音乐、第三方网易云API等等(来自GD音乐台)
|
|
||||||
|
|
||||||
const createOption = require('../util/option.js')
|
|
||||||
const logger = require('../util/logger.js')
|
|
||||||
|
|
||||||
module.exports = async (query, request) => {
|
|
||||||
try {
|
|
||||||
const match = require("@unblockneteasemusic/server")
|
|
||||||
const source = query.source
|
|
||||||
? query.source.split(',') : ['pyncmd', 'kuwo', 'qq', 'migu', 'kugou']
|
|
||||||
const server = query.server ? query.server.split(',') : query.server
|
|
||||||
const result = await match(query.id, !server? source : server)
|
|
||||||
const proxy = process.env.PROXY_URL;
|
|
||||||
logger.info("开始解灰", query.id, result)
|
|
||||||
const useProxy = process.env.ENABLE_PROXY || "false"
|
|
||||||
if (result.url.includes('kuwo') && useProxy === "true") { result.proxyUrl = proxy + result.url }
|
|
||||||
return {
|
|
||||||
status: 200,
|
|
||||||
body: {
|
|
||||||
code: 200,
|
|
||||||
data: result,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
return {
|
|
||||||
status: 500,
|
|
||||||
body: {
|
|
||||||
code: 500,
|
|
||||||
msg: e.message || 'unblock error',
|
|
||||||
data: [],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+37
-30
@@ -1,13 +1,14 @@
|
|||||||
// 歌曲链接 - v1
|
// 歌曲链接 - v1
|
||||||
// 此版本不再采用 br 作为音质区分的标准
|
// 此版本不再采用 br 作为音质区分的标准
|
||||||
// 而是采用 standard, exhigh, lossless, hires, jyeffect(高清环绕声), sky(沉浸环绕声), jymaster(超清母带) 进行音质判断
|
// 而是采用 standard, exhigh, lossless, hires, jyeffect(高清环绕声), sky(沉浸环绕声), jymaster(超清母带) 进行音质判断
|
||||||
// 当unblock为true时, 会尝试使用unblockneteasemusic进行解锁, 同时音质设置不会生效, 但仍然为必须传入参数
|
// 当unblock为true时, 会尝试使用unblockmusic-utils进行解锁, 同时音质设置不会生效, 但仍然为必须传入参数
|
||||||
|
|
||||||
const logger = require('../util/logger.js')
|
const logger = require('../util/logger.js')
|
||||||
const createOption = require('../util/option.js')
|
const createOption = require('../util/option.js')
|
||||||
module.exports = async (query, request) => {
|
module.exports = async (query, request) => {
|
||||||
const match = require('@unblockneteasemusic/server')
|
const {
|
||||||
const source = ['pyncmd','kuwo', 'qq', 'migu', 'kugou']
|
matchID,
|
||||||
|
} = require('@neteasecloudmusicapienhanced/unblockmusic-utils')
|
||||||
require('dotenv').config()
|
require('dotenv').config()
|
||||||
const data = {
|
const data = {
|
||||||
ids: '[' + query.id + ']',
|
ids: '[' + query.id + ']',
|
||||||
@@ -16,39 +17,45 @@ module.exports = async (query, request) => {
|
|||||||
}
|
}
|
||||||
if (query.unblock === 'true') {
|
if (query.unblock === 'true') {
|
||||||
try {
|
try {
|
||||||
const result = await match(query.id, source)
|
const result = await matchID(query.id, query.source)
|
||||||
logger.info('开始解灰', query.id, result)
|
logger.info('Starting unblock(uses modules unblock):', query.id, result)
|
||||||
if (result.url.includes('kuwo')) {
|
const useProxy = process.env.ENABLE_PROXY || 'false'
|
||||||
const useProxy = process.env.ENABLE_PROXY || 'false'
|
let proxyUrl = ''
|
||||||
var proxyUrl = useProxy === 'true' ? process.env.PROXY_URL + result.url : result.url
|
if (result.data.url && result.data.url.includes('kuwo')) {
|
||||||
|
proxyUrl =
|
||||||
|
useProxy === 'true' && process.env.PROXY_URL
|
||||||
|
? process.env.PROXY_URL + result.data.url
|
||||||
|
: result.data.url
|
||||||
}
|
}
|
||||||
let url = Array.isArray(result) ? (result[0]?.url || result[0]) : (result.url || result)
|
return {
|
||||||
if (url) {
|
status: 200,
|
||||||
return {
|
body: {
|
||||||
status: 200,
|
code: 200,
|
||||||
body: {
|
msg: 'Warning: Customizing unblock sources is not supported on this endpoint. Please use `/song/url/match` instead.',
|
||||||
code: 200,
|
data: [
|
||||||
data: [
|
{
|
||||||
{
|
id: Number(query.id),
|
||||||
id: Number(query.id),
|
url: result.data.url,
|
||||||
url,
|
type: 'flac',
|
||||||
type: 'flac',
|
level: query.level,
|
||||||
level: query.level,
|
freeTrialInfo: 'null',
|
||||||
freeTrialInfo: 'null',
|
fee: 0,
|
||||||
fee: 0,
|
proxyUrl: proxyUrl || '',
|
||||||
proxyUrl: proxyUrl || '',
|
},
|
||||||
},
|
],
|
||||||
],
|
},
|
||||||
},
|
cookie: [],
|
||||||
cookie: [],
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error in unblockneteasemusic:', e)
|
console.error('Error in unblocking music:', e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (data.level == 'sky') {
|
if (data.level == 'sky') {
|
||||||
data.immerseType = 'c51'
|
data.immerseType = 'c51'
|
||||||
}
|
}
|
||||||
return request(`/api/song/enhance/player/url/v1`, data, createOption(query))
|
return request(
|
||||||
|
`/api/song/enhance/player/url/v1`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'xeapi'),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
// 获取客户端歌曲下载链接 - v1
|
||||||
|
// 此版本不再采用 br 作为音质区分的标准
|
||||||
|
// 而是采用 standard, exhigh, lossless, hires, jyeffect(高清环绕声), sky(沉浸环绕声), jymaster(超清母带) 进行音质判断
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = async (query, request) => {
|
||||||
|
const data = {
|
||||||
|
id: query.id,
|
||||||
|
immerseType: 'c51',
|
||||||
|
level: query.level,
|
||||||
|
}
|
||||||
|
const response = await request(
|
||||||
|
`/api/song/enhance/download/url/v1`,
|
||||||
|
data,
|
||||||
|
createOption(query),
|
||||||
|
)
|
||||||
|
let url = response?.body?.data?.[0]?.url
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
const fallbackData = {
|
||||||
|
ids: `[${query.id}]`,
|
||||||
|
level: query.level,
|
||||||
|
encodeType: 'flac',
|
||||||
|
}
|
||||||
|
if (query.level === 'sky') {
|
||||||
|
fallbackData.immerseType = 'c51'
|
||||||
|
}
|
||||||
|
const fallback = await request(
|
||||||
|
`/api/song/enhance/player/url/v1`,
|
||||||
|
fallbackData,
|
||||||
|
createOption(query),
|
||||||
|
)
|
||||||
|
url = fallback?.body?.data?.[0]?.url
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: 302,
|
||||||
|
body: '',
|
||||||
|
cookie: fallback.cookie || [],
|
||||||
|
redirectUrl: url,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: 302,
|
||||||
|
body: '',
|
||||||
|
cookie: response.cookie || [],
|
||||||
|
redirectUrl: url,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
// 所有榜单内容摘要v2
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
return request(`/api/toplist/detail/v2`, {}, createOption(query, 'weapi'))
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
// 用户详情
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = async (query, request) => {
|
||||||
|
const data = {
|
||||||
|
all: 'true',
|
||||||
|
userId: query.uid,
|
||||||
|
}
|
||||||
|
const res = await request(
|
||||||
|
`/api/w/v1/user/detail/${query.uid}`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'eapi'),
|
||||||
|
)
|
||||||
|
// const result = JSON.stringify(res).replace(
|
||||||
|
// /avatarImgId_str/g,
|
||||||
|
// "avatarImgIdStr"
|
||||||
|
// );
|
||||||
|
// return JSON.parse(result);
|
||||||
|
return res
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ module.exports = (query, request) => {
|
|||||||
const data = {
|
const data = {
|
||||||
userId: query.uid,
|
userId: query.uid,
|
||||||
time: '0',
|
time: '0',
|
||||||
limit: query.limit || 30,
|
limit: query.limit || 20,
|
||||||
offset: query.offset || 0,
|
offset: query.offset || 0,
|
||||||
getcounts: 'true',
|
getcounts: 'true',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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))
|
||||||
|
}
|
||||||
@@ -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))
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// 黑胶乐签打卡
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {}
|
||||||
|
return request(
|
||||||
|
`/api/vip-center-bff/task/sign`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'weapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
// 黑胶乐签打卡详情
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
signDayTime: query.timestamp,
|
||||||
|
type: '1',
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
`/api/vipnewcenter/app/level/user/checkin/history/detail`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'eapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
// 黑胶乐签打卡历史
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
type: '0',
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
`/api/vipnewcenter/app/minidesk/music/sign/pc`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'xeapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// 黑胶乐签未来签到信息
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {}
|
||||||
|
return request(
|
||||||
|
`/api/vipnewcenter/app/user/sign/info`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'weapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
// 会员任务 - 新版
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
taskType: 'app_vip_task_center',
|
||||||
|
userId: query.id,
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
`/api/middle/vip/mission/user/progress/list`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'xeapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
+36
-22
@@ -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
|
||||||
offset,
|
if (useTempFile) {
|
||||||
Math.min(offset + blockSize, fileSize),
|
chunk = await readFileChunk(
|
||||||
)
|
query.songFile.tempFilePath,
|
||||||
|
offset,
|
||||||
|
Math.min(blockSize, fileSize - offset),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
chunk = query.songFile.data.slice(
|
||||||
|
offset,
|
||||||
|
Math.min(offset + blockSize, fileSize),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const 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`,
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
// 我创建的播客声音
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
limit: query.limit || 20,
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
`/api/social/my/created/voicelist/v1`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'weapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,14 +1,11 @@
|
|||||||
const createOption = require('../util/option.js')
|
const createOption = require('../util/option.js')
|
||||||
module.exports = (query, request) => {
|
module.exports = (query, request) => {
|
||||||
const data = {
|
const data = {
|
||||||
fee: '-1',
|
keyword: query.keyword || '',
|
||||||
limit: query.limit || '200',
|
scene: 'normal',
|
||||||
offset: query.offset || '0',
|
limit: query.limit || '10',
|
||||||
podcastName: query.podcastName || '',
|
offset: query.offset || '30',
|
||||||
|
e_r: true,
|
||||||
}
|
}
|
||||||
return request(
|
return request(`/api/search/voicelist/get`, data, createOption(query))
|
||||||
`/api/voice/workbench/voicelist/search`,
|
|
||||||
data,
|
|
||||||
createOption(query),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { banner, lyric } from '@neteaseapireborn/api'
|
import { banner, lyric } from '@neteasecloudmusicapienhanced/api'
|
||||||
import { logger } from '../util/logger.js'
|
import { logger } from '../util/logger.js'
|
||||||
banner({ type: 0 }).then((res) => {
|
banner({ type: 0 }).then((res) => {
|
||||||
logger.info(res)
|
logger.info(res)
|
||||||
|
|||||||
Generated
-11051
File diff suppressed because it is too large
Load Diff
+53
-37
@@ -1,16 +1,17 @@
|
|||||||
{
|
{
|
||||||
"name": "@neteaseapireborn/api",
|
"name": "@neteasecloudmusicapienhanced/api",
|
||||||
"version": "4.28.29",
|
"version": "4.35.1",
|
||||||
"description": "为停更的网易云音乐 NodeJs API 提供持续的维护!",
|
"description": "全网最全的网易云音乐API接口 || A revival project for NeteaseCloudMusicApi Node.js Services (Half Refactor & Enhanced) || 网易云音乐 API 备份 + 增强 || 本项目自原版v4.28.0版本后开始自行维护",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"dev": "nodemon app.js",
|
||||||
"start": "node app.js",
|
"start": "node app.js",
|
||||||
"test": "mocha -r intelli-espower-loader -t 60000 server.test.js main.test.js --exit",
|
"test": "mocha -r intelli-espower-loader -t 60000 server.test.js main.test.js --exit",
|
||||||
"lint": "eslint \"**/*.{js,ts}\"",
|
"lint": "eslint \"**/*.{js,ts}\"",
|
||||||
"lint-fix": "eslint --fix \"**/*.{js,ts}\"",
|
"lint-fix": "eslint --fix \"**/*.{js,ts}\"",
|
||||||
"prepare": "husky install",
|
"prepare": "husky install",
|
||||||
"pkgwin": "pkg . -t node18-win-x64 -C GZip -o bin/app --no-bytecode",
|
"pkgwin": "pkg . -t node18-win-x64 -C GZip -o precompiled/app",
|
||||||
"pkglinux": "pkg . -t node18-linux-x64 -C GZip -o bin/app --no-bytecode",
|
"pkglinux": "pkg . -t node18-linux-x64 -C GZip -o precompiled/app",
|
||||||
"pkgmacos": "pkg . -t node18-macos-x64 -C GZip -o bin/app --no-bytecode"
|
"pkgmacos": "pkg . -t node18-macos-x64 -C GZip -o precompiled/app"
|
||||||
},
|
},
|
||||||
"bin": "./app.js",
|
"bin": "./app.js",
|
||||||
"pkg": {
|
"pkg": {
|
||||||
@@ -19,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",
|
||||||
@@ -41,6 +41,14 @@
|
|||||||
],
|
],
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"types": "./interface.d.ts",
|
"types": "./interface.d.ts",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/NeteaseCloudMusicApiEnhanced/api-enhanced.git"
|
||||||
|
},
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/NeteaseCloudMusicApiEnhanced/api-enhanced/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://neteasecloudmusicapienhanced.js.org/",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -50,7 +58,7 @@
|
|||||||
"git add"
|
"git add"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"author": "binaryify",
|
"author": "MoeFurina",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"files": [
|
"files": [
|
||||||
"module",
|
"module",
|
||||||
@@ -65,40 +73,48 @@
|
|||||||
"data"
|
"data"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@unblockneteasemusic/server": "^0.27.10",
|
"@neteasecloudmusicapienhanced/unblockmusic-utils": "^0.3.2",
|
||||||
"axios": "^1.2.2",
|
"axios": "^1.17.0",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"dotenv": "^16.0.3",
|
"dotenv": "^17.4.2",
|
||||||
"express": "^4.17.1",
|
"express": "^5.2.1",
|
||||||
"express-fileupload": "^1.1.9",
|
"express-fileupload": "^1.5.2",
|
||||||
"md5": "^2.3.0",
|
"gzip": "^0.1.0",
|
||||||
"music-metadata": "^7.5.3",
|
"music-metadata": "^11.13.0",
|
||||||
"node-forge": "^1.3.1",
|
"node-forge": "^1.4.0",
|
||||||
"pac-proxy-agent": "^7.0.0",
|
"pac-proxy-agent": "^7.2.0",
|
||||||
"qrcode": "^1.4.4",
|
"qrcode": "^1.5.4",
|
||||||
"safe-decode-uri-component": "^1.2.1",
|
"safe-decode-uri-component": "^1.2.1",
|
||||||
"tunnel": "^0.0.6",
|
"tunnel": "^0.0.6",
|
||||||
"xml2js": "^0.6.2",
|
"xml2js": "^0.6.2",
|
||||||
"yargs": "^17.1.1"
|
"yargs": "^18.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/express": "^4.17.13",
|
"@eslint/eslintrc": "^3.3.5",
|
||||||
"@types/express-fileupload": "^1.2.2",
|
"@eslint/js": "^9.39.4",
|
||||||
"@types/mocha": "^9.1.0",
|
"@types/express": "^5.0.6",
|
||||||
"@types/node": "16.11.19",
|
"@types/express-fileupload": "^1.5.1",
|
||||||
"@typescript-eslint/eslint-plugin": "5.0.0",
|
"@types/mocha": "^10.0.10",
|
||||||
"@typescript-eslint/parser": "5.0.0",
|
"@types/node": "25.9.2",
|
||||||
"eslint": "8.7.0",
|
"@typescript-eslint/eslint-plugin": "^8.60.1",
|
||||||
"eslint-config-prettier": "8.5.0",
|
"@typescript-eslint/parser": "^8.60.1",
|
||||||
"eslint-plugin-html": "7.1.0",
|
"eslint": "^9.39.4",
|
||||||
"eslint-plugin-prettier": "4.0.0",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"husky": "7.0.4",
|
"eslint-plugin-html": "^8.1.4",
|
||||||
"intelli-espower-loader": "1.1.0",
|
"eslint-plugin-prettier": "^5.5.6",
|
||||||
"lint-staged": "12.1.7",
|
"globals": "^17.6.0",
|
||||||
"mocha": "10.0.0",
|
"husky": "^9.1.7",
|
||||||
"pkg": "^5.8.0",
|
"intelli-espower-loader": "^1.1.0",
|
||||||
"power-assert": "1.6.1",
|
"lint-staged": "^16.4.0",
|
||||||
"prettier": "2.7.1",
|
"mocha": "^11.7.6",
|
||||||
"typescript": "4.5.2"
|
"nodemon": "^3.1.14",
|
||||||
|
"pkg": "^5.8.1",
|
||||||
|
"power-assert": "^1.6.1",
|
||||||
|
"prettier": "^3.8.3",
|
||||||
|
"typescript": "^5.9.3"
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public",
|
||||||
|
"registry": "https://registry.npmjs.org/"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+67
-19
@@ -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,
|
||||||
|
|||||||
+5
-7
@@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+1511
-1125
File diff suppressed because it is too large
Load Diff
+145
-49
@@ -5,82 +5,149 @@
|
|||||||
<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;
|
min-height: 100vh;
|
||||||
display: flex;
|
background: #f5f5f5;
|
||||||
flex-direction: column;
|
padding: 20px;
|
||||||
min-height: 100vh;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.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;
|
||||||
align-items: center;
|
gap: 16px;
|
||||||
gap: 10px;
|
margin-bottom: 24px;
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
}
|
||||||
input, button {
|
|
||||||
padding: 10px;
|
.form-row {
|
||||||
box-sizing: border-box;
|
display: flex;
|
||||||
flex: 1;
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #555;
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input, select {
|
||||||
|
padding: 10px 14px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
flex: 1;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus, select:focus {
|
||||||
|
border-color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
background-color: #4CAF50;
|
background: #333;
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
padding: 10px 24px;
|
||||||
cursor: pointer;
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
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 {
|
||||||
display: flex;
|
flex: 1;
|
||||||
flex-direction: column;
|
display: flex;
|
||||||
flex-grow: 1;
|
flex-direction: column;
|
||||||
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">
|
||||||
<input type="text" id="uri" name="uri" value="/api/song/lyric/v1">
|
<label for="uri">URI</label>
|
||||||
<label for="crypto">crypto</label>
|
<input type="text" id="uri" name="uri" value="/api/song/lyric/v1">
|
||||||
<select id="crypto" name="crypto">
|
</div>
|
||||||
<option value="weapi">weapi</option>
|
<div class="form-row">
|
||||||
<option value="eapi">eapi</option>
|
<label for="crypto">加密方式</label>
|
||||||
<option value="api">api</option>
|
<select id="crypto" name="crypto">
|
||||||
<option value="linuxapi">linuxapi</option>
|
<option value="weapi">weapi</option>
|
||||||
<option value="" selected>(默认)</option>
|
<option value="eapi">eapi</option>
|
||||||
</select>
|
<option value="api">api</option>
|
||||||
<button type="submit">发送</button>
|
<option value="linuxapi">linuxapi</option>
|
||||||
|
<option value="xeapi">xeapi</option>
|
||||||
|
<option value="" selected>(默认)</option>
|
||||||
|
</select>
|
||||||
|
</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,
|
||||||
@@ -122,6 +189,35 @@
|
|||||||
document.getElementById('result').value = 'Request failed: ' + error.message;
|
document.getElementById('result').value = 'Request failed: ' + error.message;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
(function fillFromQuery() {
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
if (!params.toString()) return;
|
||||||
|
const uri = params.get('uri');
|
||||||
|
const crypto = params.get('crypto');
|
||||||
|
const dataParam = params.get('data');
|
||||||
|
if (uri) {
|
||||||
|
document.getElementById('uri').value = uri;
|
||||||
|
}
|
||||||
|
if (crypto) {
|
||||||
|
const cryptoSelect = document.getElementById('crypto');
|
||||||
|
if ([...cryptoSelect.options].some((opt) => opt.value === crypto)) {
|
||||||
|
cryptoSelect.value = crypto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (dataParam) {
|
||||||
|
const decoded = dataParam;
|
||||||
|
try {
|
||||||
|
document.getElementById('data').value = JSON.stringify(
|
||||||
|
JSON.parse(decoded),
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
document.getElementById('data').value = decoded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -0,0 +1,695 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>API 参数和返回内容解析</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f5f5f5;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 32px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #555;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 200px;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea:focus {
|
||||||
|
border-color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 24px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-item input[type="radio"] {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-item label {
|
||||||
|
margin: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background: #333;
|
||||||
|
color: white;
|
||||||
|
padding: 12px 28px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
button + button {
|
||||||
|
margin-left: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-section {
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-section label {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.decode-result {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
background: #f9f9f9;
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
min-height: 200px;
|
||||||
|
max-height: 400px;
|
||||||
|
overflow: auto;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.decode-result .json-key { color: #881391; }
|
||||||
|
.decode-result .json-string { color: #1a7f37; }
|
||||||
|
.decode-result .json-number { color: #0550ae; }
|
||||||
|
.decode-result .json-boolean { color: #cf222e; }
|
||||||
|
.decode-result .json-null { color: #656d76; }
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* new elements for multi-crypto support */
|
||||||
|
.crypto-bar {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: #fafafa;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.crypto-bar .bar-label {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #555;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.crypto-btn {
|
||||||
|
padding: 5px 14px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
background: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #666;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.crypto-btn:hover {
|
||||||
|
border-color: #333;
|
||||||
|
color: #333;
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.crypto-btn.active {
|
||||||
|
border-color: #333;
|
||||||
|
background: #333;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.crypto-btn .badge {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 10px;
|
||||||
|
background: rgba(0,0,0,0.08);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 0 5px;
|
||||||
|
margin-left: 3px;
|
||||||
|
}
|
||||||
|
.crypto-btn.active .badge {
|
||||||
|
background: rgba(255,255,255,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-row .mode-label {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-row .radio-group {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-hint {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #888;
|
||||||
|
margin-top: 6px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
background: #fafafa;
|
||||||
|
border-radius: 4px;
|
||||||
|
border-left: 3px solid #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-hint.warn {
|
||||||
|
border-left-color: #e67e22;
|
||||||
|
background: #fef9f0;
|
||||||
|
color: #d35400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-row button + button {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.btn-secondary {
|
||||||
|
background: #e8e8e8;
|
||||||
|
color: #444;
|
||||||
|
}
|
||||||
|
button.btn-secondary:hover {
|
||||||
|
background: #d5d5d5;
|
||||||
|
}
|
||||||
|
button.btn-secondary:disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
button.btn-success {
|
||||||
|
background: #4caf50;
|
||||||
|
}
|
||||||
|
button.btn-success:hover {
|
||||||
|
background: #43a047;
|
||||||
|
}
|
||||||
|
button.btn-success:disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-section .section-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-section .section-header label {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-btn {
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 3px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
background: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #666;
|
||||||
|
transition: all 0.15s;
|
||||||
|
}
|
||||||
|
.copy-btn:hover {
|
||||||
|
background: #333;
|
||||||
|
color: #fff;
|
||||||
|
border-color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-card {
|
||||||
|
background: #fafafa;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 16px;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-card h3 {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #555;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-card pre {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
background: #fff;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow-x: auto;
|
||||||
|
border: 1px solid #e8e8e8;
|
||||||
|
max-height: 120px;
|
||||||
|
overflow-y: auto;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-card .tag {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 3px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-eapi { background: #e3f2fd; color: #1565c0; }
|
||||||
|
.tag-weapi { background: #fce4ec; color: #c62828; }
|
||||||
|
.tag-linuxapi { background: #e8f5e9; color: #2e7d32; }
|
||||||
|
.tag-xeapi { background: #f3e5f5; color: #7b1fa2; }
|
||||||
|
.tag-api { background: #fff3e0; color: #e65100; }
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.example-grid { grid-template-columns: 1fr; }
|
||||||
|
.crypto-bar { gap: 4px; }
|
||||||
|
.crypto-btn { padding: 4px 10px; font-size: 12px; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="app" class="container">
|
||||||
|
<h1>API 参数和返回内容解析</h1>
|
||||||
|
|
||||||
|
<!-- 加密方式选择 -->
|
||||||
|
<div class="crypto-bar">
|
||||||
|
<span class="bar-label">加密方式</span>
|
||||||
|
<button
|
||||||
|
v-for="c in cryptoList"
|
||||||
|
:key="c.value"
|
||||||
|
class="crypto-btn"
|
||||||
|
:class="{ active: crypto === c.value }"
|
||||||
|
@click="selectCrypto(c.value)"
|
||||||
|
>
|
||||||
|
{{ c.label }}
|
||||||
|
<span class="badge">{{ c.badge }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mode-row">
|
||||||
|
<span class="mode-label">数据类型</span>
|
||||||
|
<div class="radio-group">
|
||||||
|
<div class="radio-item">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
id="mode-req"
|
||||||
|
:value="true"
|
||||||
|
v-model="isReq"
|
||||||
|
:disabled="cryptoInfo.reqDisabled"
|
||||||
|
/>
|
||||||
|
<label for="mode-req">请求数据 request</label>
|
||||||
|
</div>
|
||||||
|
<div class="radio-item">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
id="mode-resp"
|
||||||
|
:value="false"
|
||||||
|
v-model="isReq"
|
||||||
|
:disabled="cryptoInfo.respDisabled"
|
||||||
|
/>
|
||||||
|
<label for="mode-resp">返回数据 response</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="dataInput">{{ cryptoInfo.inputLabel }}</label>
|
||||||
|
<textarea
|
||||||
|
id="dataInput"
|
||||||
|
v-model="encryptedData"
|
||||||
|
:rows="crypto === 'xeapi' ? 8 : 10"
|
||||||
|
:placeholder="cryptoInfo.placeholder"
|
||||||
|
></textarea>
|
||||||
|
<div class="info-hint" :class="{ warn: cryptoInfo.warning }">
|
||||||
|
{{ cryptoInfo.hint }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="action-row">
|
||||||
|
<button @click="decrypt">解密</button>
|
||||||
|
<button class="btn-success" @click="sendToApi" :disabled="!canSend">
|
||||||
|
填入 API 调试
|
||||||
|
</button>
|
||||||
|
<button class="btn-secondary" @click="loadExample">加载示例</button>
|
||||||
|
<button class="btn-secondary" @click="clearAll">清空</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="result-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<label>解密结果:</label>
|
||||||
|
<button
|
||||||
|
v-if="result && result !== '{}' && result !== 'null'"
|
||||||
|
class="copy-btn"
|
||||||
|
@click="copyResult"
|
||||||
|
>
|
||||||
|
复制
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<pre class="decode-result" v-html="highlightedResult"></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="example-section">
|
||||||
|
<h2>使用示例</h2>
|
||||||
|
<img src="/static/eapi_params.png" alt="请求示例" />
|
||||||
|
<img src="/static/eapi_response.png" alt="响应示例" />
|
||||||
|
|
||||||
|
<div class="example-grid">
|
||||||
|
<div class="example-card">
|
||||||
|
<span class="tag tag-eapi">eapi</span>
|
||||||
|
<h3>请求参数解密</h3>
|
||||||
|
<pre>输入 hex 字符串,解密后得到请求 URL 和参数</pre>
|
||||||
|
<button class="copy-btn" @click="loadExampleData('eapi_req')">加载示例</button>
|
||||||
|
</div>
|
||||||
|
<div class="example-card">
|
||||||
|
<span class="tag tag-linuxapi">linuxapi</span>
|
||||||
|
<h3>请求 eparams 解密</h3>
|
||||||
|
<pre>AES-ECB 解密 linuxapi 的 eparams 参数(hex)</pre>
|
||||||
|
<button class="copy-btn" @click="loadExampleData('linuxapi_req')">加载示例</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://fastly.jsdelivr.net/npm/axios"></script>
|
||||||
|
<script src="https://fastly.jsdelivr.net/npm/vue@3"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const cryptoInfos = {
|
||||||
|
eapi: {
|
||||||
|
label: 'eapi',
|
||||||
|
badge: 'AES-ECB',
|
||||||
|
inputLabel: '十六进制字符串(Hex)',
|
||||||
|
placeholder: '粘贴 eapi 加密后的 Hex 字符串...',
|
||||||
|
hint: 'eapi 使用 AES-ECB 加密,密钥 e82ckenh8dichen8。请求格式: url-36cd479b6b5-data-36cd479b6b5-md5',
|
||||||
|
warning: false,
|
||||||
|
reqDisabled: false,
|
||||||
|
respDisabled: false,
|
||||||
|
},
|
||||||
|
weapi: {
|
||||||
|
label: 'weapi',
|
||||||
|
badge: 'AES-CBC+RSA',
|
||||||
|
inputLabel: '十六进制字符串(Hex)',
|
||||||
|
placeholder: '粘贴 weapi 返回数据的 Hex 字符串(e_r=true 时)...',
|
||||||
|
hint: 'weapi 请求解密需要 RSA 私钥,暂不支持。仅支持返回数据解密(同 eapi 的 AES-ECB)',
|
||||||
|
warning: true,
|
||||||
|
reqDisabled: true,
|
||||||
|
respDisabled: false,
|
||||||
|
},
|
||||||
|
linuxapi: {
|
||||||
|
label: 'linuxapi',
|
||||||
|
badge: 'AES-ECB',
|
||||||
|
inputLabel: '十六进制字符串(Hex)',
|
||||||
|
placeholder: '粘贴 linuxapi 的 eparams (Hex 格式)...',
|
||||||
|
hint: 'linuxapi 请求 eparams 使用 AES-ECB 加密,密钥 rFgB&h#%2?^eDg:Q。返回为明文 JSON',
|
||||||
|
warning: false,
|
||||||
|
reqDisabled: false,
|
||||||
|
respDisabled: false,
|
||||||
|
},
|
||||||
|
xeapi: {
|
||||||
|
label: 'xeapi',
|
||||||
|
badge: 'X25519+AES',
|
||||||
|
inputLabel: 'Base64 编码字符串',
|
||||||
|
placeholder: '粘贴 xeapi 返回数据的 Base64 原始二进制...',
|
||||||
|
hint: 'xeapi 请求涉及 X25519 ECDH 密钥交换,暂不支持。返回数据使用 AES-ECB + eapiKey 加密,可能带 gzip',
|
||||||
|
warning: true,
|
||||||
|
reqDisabled: true,
|
||||||
|
respDisabled: false,
|
||||||
|
},
|
||||||
|
api: {
|
||||||
|
label: 'api',
|
||||||
|
badge: '明文',
|
||||||
|
inputLabel: 'JSON 文本',
|
||||||
|
placeholder: '粘贴 api 的请求或返回 JSON...',
|
||||||
|
hint: 'api(明文)不经加密,直接传递 JSON 数据',
|
||||||
|
warning: false,
|
||||||
|
reqDisabled: false,
|
||||||
|
respDisabled: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const examples = {
|
||||||
|
eapi_req:
|
||||||
|
'AD96DDB984491E79B6F429DD650C6E2AE524627AC223AC9A123C66BB0997965950FED137544A93DFC718E16F57C8C121AF537086F395570A5602A3922366D11964DAFACD7830AACABF62E5650E67F457E79C1D2E13502391FC3487216CC5BF8681843FCB8E05559487EB18AAC1BE0EFEA4F7B6A050478366153A9426C238B8869600B275704555A9EB94C92E4F3FDABE9E0BCE07645410D0AA7B675698A4CAE6CD3620633ABF0B849A4244CC8DFC5DB2646D5EA9B3954E62BFEF19AFEAFDDC34E55C3E9A1DD3167CF53D443617108141',
|
||||||
|
linuxapi_req:
|
||||||
|
'A0D9583F4C5FF68DE851D2893A49DE98CC059A8845B664AA2459CEA7271A2C0E5BCA8A188E1BE398DB4C9A3FC117E19C9BAC491F454D17D403C2389476AB0FF4296B00294AD1EBDA141C188DF918F6B9599DAA5739928FD52B4AE580D8657903CE3C6633D2E46AD242408AE219B8191E',
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = Vue.createApp({
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
crypto: 'eapi',
|
||||||
|
encryptedData: '',
|
||||||
|
result: '{}',
|
||||||
|
isReq: true,
|
||||||
|
cryptoList: [
|
||||||
|
{ value: 'eapi', label: 'eapi', badge: 'AES-ECB' },
|
||||||
|
{ value: 'weapi', label: 'weapi', badge: 'AES-CBC+RSA' },
|
||||||
|
{ value: 'linuxapi', label: 'linuxapi', badge: 'AES-ECB' },
|
||||||
|
{ value: 'xeapi', label: 'xeapi', badge: 'X25519+AES' },
|
||||||
|
{ value: 'api', label: 'api', badge: '明文' },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.loadExample()
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
cryptoInfo() {
|
||||||
|
return cryptoInfos[this.crypto] || cryptoInfos.eapi
|
||||||
|
},
|
||||||
|
isRequestMode() {
|
||||||
|
return this.isReq === true || this.isReq === 'true'
|
||||||
|
},
|
||||||
|
canSend() {
|
||||||
|
if (!this.isRequestMode) return false
|
||||||
|
if (!this.result || this.result === '{}' || this.result === 'null') return false
|
||||||
|
try {
|
||||||
|
JSON.parse(this.result)
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
highlightedResult() {
|
||||||
|
if (!this.result || this.result === '') return ''
|
||||||
|
try {
|
||||||
|
const parsed = typeof this.result === 'string' ? JSON.parse(this.result) : this.result
|
||||||
|
const json = JSON.stringify(parsed, null, 2)
|
||||||
|
return this.syntaxHighlight(json)
|
||||||
|
} catch (error) {
|
||||||
|
return this.escapeHtml(String(this.result))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
crypto(val) {
|
||||||
|
const info = cryptoInfos[val]
|
||||||
|
if (this.isReq && info.reqDisabled) {
|
||||||
|
this.isReq = false
|
||||||
|
} else if (!this.isReq && info.respDisabled) {
|
||||||
|
this.isReq = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
selectCrypto(val) {
|
||||||
|
this.crypto = val
|
||||||
|
},
|
||||||
|
syntaxHighlight(json) {
|
||||||
|
const escaped = this.escapeHtml(json)
|
||||||
|
return escaped
|
||||||
|
.replace(/(?:"(?:\\.|[^"\\])*")\s*:/g, '<span class="json-key">$&</span>')
|
||||||
|
.replace(/:(\s*)(?:"(?:\\.|[^"\\])*")/g, ':<span class="json-string">$1$2</span>')
|
||||||
|
.replace(/:\s*(\d+(?:\.\d+)?)/g, ': <span class="json-number">$1</span>')
|
||||||
|
.replace(/:\s*(true|false)/g, ': <span class="json-boolean">$1</span>')
|
||||||
|
.replace(/:\s*(null)/g, ': <span class="json-null">$1</span>')
|
||||||
|
},
|
||||||
|
escapeHtml(text) {
|
||||||
|
const div = document.createElement('div')
|
||||||
|
div.textContent = text
|
||||||
|
return div.innerHTML
|
||||||
|
},
|
||||||
|
formatResult(value) {
|
||||||
|
if (value == null || value === '') return ''
|
||||||
|
try {
|
||||||
|
const parsed = typeof value === 'string' ? JSON.parse(value) : value
|
||||||
|
return JSON.stringify(parsed, null, 2)
|
||||||
|
} catch (error) {
|
||||||
|
return String(value)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async decrypt() {
|
||||||
|
if (!this.encryptedData || !this.encryptedData.trim()) {
|
||||||
|
alert('请先输入要解密的数据')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await axios({
|
||||||
|
url: `/decrypt?crypto=${this.crypto}&isReq=${this.isReq}×tamp=${Date.now()}`,
|
||||||
|
method: 'post',
|
||||||
|
data: { data: this.encryptedData },
|
||||||
|
})
|
||||||
|
this.result = JSON.stringify(res.data.data)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
const msg = error?.response?.data?.message || error?.message || '解密失败,数据格式错误'
|
||||||
|
alert(msg)
|
||||||
|
this.result = '{}'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
sendToApi() {
|
||||||
|
if (!this.canSend) return
|
||||||
|
const payload = JSON.parse(this.result)
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
const uri = payload.uri || payload.url || payload.path || ''
|
||||||
|
params.set('uri', uri)
|
||||||
|
params.set('crypto', this.crypto)
|
||||||
|
const data = payload.params || payload.data || payload.body || payload.payload || payload.request || {}
|
||||||
|
params.set('data', JSON.stringify(data))
|
||||||
|
window.open(`/api.html?${params.toString()}`, '_blank')
|
||||||
|
},
|
||||||
|
loadExample() {
|
||||||
|
switch (this.crypto) {
|
||||||
|
case 'eapi':
|
||||||
|
this.encryptedData = examples.eapi_req
|
||||||
|
break
|
||||||
|
case 'linuxapi':
|
||||||
|
this.isReq = true
|
||||||
|
this.encryptedData = examples.linuxapi_req
|
||||||
|
break
|
||||||
|
case 'xeapi':
|
||||||
|
this.isReq = false
|
||||||
|
this.encryptedData = ''
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
this.encryptedData = ''
|
||||||
|
}
|
||||||
|
if (this.encryptedData) this.decrypt()
|
||||||
|
},
|
||||||
|
loadExampleData(type) {
|
||||||
|
switch (type) {
|
||||||
|
case 'eapi_req':
|
||||||
|
this.crypto = 'eapi'
|
||||||
|
this.isReq = true
|
||||||
|
this.encryptedData = examples.eapi_req
|
||||||
|
break
|
||||||
|
case 'linuxapi_req':
|
||||||
|
this.crypto = 'linuxapi'
|
||||||
|
this.isReq = true
|
||||||
|
this.encryptedData = examples.linuxapi_req
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (this.encryptedData) this.decrypt()
|
||||||
|
},
|
||||||
|
clearAll() {
|
||||||
|
this.encryptedData = ''
|
||||||
|
this.result = '{}'
|
||||||
|
},
|
||||||
|
copyResult() {
|
||||||
|
const text = typeof this.result === 'string' ? this.result : JSON.stringify(this.result, null, 2)
|
||||||
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
|
alert('已复制到剪贴板')
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
app.mount('#app')
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -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">
|
||||||
<hr>
|
<h1>听歌识曲 Demo</h1>
|
||||||
<p><b>DISCLAIMER: </b></p>
|
<p class="subtitle">Credit: <a href="https://github.com/mos9527/ncm-afp" target="_blank">https://github.com/mos9527/ncm-afp</a></p>
|
||||||
<p>This site uses the offical NetEase audio matcher APIs (reverse engineered from <a
|
|
||||||
href="https://fn.music.163.com/g/chrome-extension-home-page-beta/">https://fn.music.163.com/g/chrome-extension-home-page-beta/</a>)
|
|
||||||
</p>
|
|
||||||
<p>And DOES NOT condone copyright infringment nor intellectual property theft.</p>
|
|
||||||
<hr>
|
|
||||||
<p><b>NOTE:</b></p>
|
|
||||||
<p>Before start using the site, you may want to access this link first:</p>
|
|
||||||
<a href="https://cors-anywhere.herokuapp.com/corsdemo">https://cors-anywhere.herokuapp.com/corsdemo</a>
|
|
||||||
<p>Since Netease APIs do not have CORS headers, this is required to alleviate this restriction.</p>
|
|
||||||
<hr>
|
|
||||||
<p>Usage:</p>
|
|
||||||
<li>Select your audio file through "Choose File" picker</li>
|
|
||||||
<li>Hit the "Clip" button and wait for the results!</li>
|
|
||||||
|
|
||||||
<audio id="audio" controls autoplay></audio>
|
<hr>
|
||||||
<canvas id="canvas"></canvas>
|
|
||||||
<button id="invoke">Clip</button>
|
<div class="warning">
|
||||||
<input type="file" name="picker" accept="*" id="file">
|
<strong>免责声明:</strong>本站点使用网易云音乐官方音频识别API(逆向自 <a href="https://fn.music.163.com/g/chrome-extension-home-page-beta/" target="_blank">Chrome 扩展页面</a>),不鼓励版权侵犯或知识产权盗窃。
|
||||||
<hr>
|
</div>
|
||||||
<label for="use-mic">Mix in Microphone input</label>
|
|
||||||
<input type="checkbox" name="use-mic" id="usemic">
|
<div class="section">
|
||||||
<hr>
|
<h3>使用说明</h3>
|
||||||
<pre id="logs"></pre>
|
<p>在使用本站点之前,您可能需要先访问以下链接:</p>
|
||||||
|
<p><a href="https://cors-anywhere.herokuapp.com/corsdemo" target="_blank">https://cors-anywhere.herokuapp.com/corsdemo</a></p>
|
||||||
|
<p>由于网易云音乐API没有CORS头,这是解决此限制的必要步骤。</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h3>使用方法</h3>
|
||||||
|
<ul style="padding-left: 20px; font-size: 14px; color: #555;">
|
||||||
|
<li>通过"选择文件"选择您的音频文件</li>
|
||||||
|
<li>点击"识别"按钮并等待结果</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<audio id="audio" controls autoplay></audio>
|
||||||
|
<canvas id="canvas"></canvas>
|
||||||
|
|
||||||
|
<div class="control-group">
|
||||||
|
<button id="invoke">识别</button>
|
||||||
|
<input type="file" name="picker" accept="*" id="file">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-group">
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<input type="checkbox" name="use-mic" id="usemic">
|
||||||
|
<label for="usemic">混合麦克风输入</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<h3 style="font-size: 16px; font-weight: 600; color: #333; margin-bottom: 12px;">日志</h3>
|
||||||
|
<pre id="logs"></pre>
|
||||||
|
</div>
|
||||||
</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>
|
||||||
+296
-43
@@ -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>
|
||||||
|
|
||||||
|
<div class="avatar-wrapper" id="avatarWrapper">
|
||||||
|
<img id="avatar" class="avatar" src="" alt="头像" />
|
||||||
|
<div class="loading-overlay">
|
||||||
|
<div class="spinner"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label class="upload-btn">
|
||||||
|
选择图片
|
||||||
|
<input id="file" type="file" accept="image/*" />
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<a href="/qrlogin-nocookie.html" class="login-link">
|
||||||
|
还没有登录?点击登录
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
<p class="hint">支持 JPG、PNG 格式,建议尺寸 200x200</p>
|
||||||
</div>
|
</div>
|
||||||
<input id="file" type="file" />
|
|
||||||
<img id="avatar" style="height: 200px; width: 200px; border-radius: 50%" />
|
<div id="toast" class="toast"></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>
|
||||||
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);
|
||||||
)
|
}
|
||||||
const res = await axios({
|
}, false);
|
||||||
url: `/user/detail?uid=32953014×tamp=${Date.now()}`,
|
|
||||||
withCredentials: true, //跨域的话必须设置
|
try {
|
||||||
})
|
showToast('正在加载头像...', 'info');
|
||||||
document.querySelector('#avatar').src = res.data.profile.avatarUrl
|
avatarWrapper.classList.add('loading');
|
||||||
|
const res = await axios({
|
||||||
|
url: `/user/detail?uid=32953014×tamp=${Date.now()}`,
|
||||||
|
withCredentials: true
|
||||||
|
});
|
||||||
|
document.querySelector('#avatar').src = res.data.profile.avatarUrl;
|
||||||
|
hideToast();
|
||||||
|
} catch (error) {
|
||||||
|
hideToast();
|
||||||
|
showToast('加载头像失败,请刷新页面重试', 'error');
|
||||||
|
console.error('加载头像失败:', error);
|
||||||
|
} finally {
|
||||||
|
avatarWrapper.classList.remove('loading');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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/')) {
|
||||||
const res = await axios({
|
showToast('请选择图片文件', 'error');
|
||||||
method: 'post',
|
return;
|
||||||
url: `/avatar/upload?cookie=${localStorage.getItem('cookie')}&imgSize=${imgSize.width
|
}
|
||||||
}&imgX=0&imgY=0×tamp=${Date.now()}`,
|
|
||||||
headers: {
|
try {
|
||||||
'Content-Type': 'multipart/form-data',
|
showToast('正在上传头像...', 'info');
|
||||||
},
|
avatarWrapper.classList.add('loading');
|
||||||
data: formData,
|
|
||||||
})
|
var formData = new FormData();
|
||||||
document.querySelector('#avatar').src = res.data.data.url
|
formData.append('imgFile', file);
|
||||||
|
const imgSize = await getImgSize(file);
|
||||||
|
|
||||||
|
const res = await axios({
|
||||||
|
method: 'post',
|
||||||
|
url: `/avatar/upload?cookie=${localStorage.getItem('cookie')}&imgSize=${imgSize.width}&imgX=0&imgY=0×tamp=${Date.now()}`,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
},
|
||||||
|
data: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelector('#avatar').src = res.data.data.url;
|
||||||
|
showToast('头像更新成功!', 'success');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('上传失败:', error);
|
||||||
|
const errorMsg = error.response?.data?.message || error.message || '上传失败,请重试';
|
||||||
|
showToast(errorMsg, 'error');
|
||||||
|
} finally {
|
||||||
|
avatarWrapper.classList.remove('loading');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user