mirror of
https://github.com/NeteaseCloudMusicApiEnhanced/api-enhanced.git
synced 2026-10-20 22:51:23 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1bdc85998b | ||
|
|
49c7800afe | ||
|
|
19c7dad6db | ||
|
|
2db6684453 | ||
|
|
90de64140f | ||
|
|
2aab9957df | ||
|
|
373d5eedb3 | ||
|
|
c1cbdcb962 | ||
|
|
37b64ab01e | ||
|
|
95367213f7 | ||
|
|
bd407cee81 | ||
|
|
a91ea8f832 | ||
|
|
4a45a14aa7 | ||
|
|
acbea014be | ||
|
|
8ad30b591d | ||
|
|
cb449463fb | ||
|
|
91db12d0f5 | ||
|
|
135df9edda | ||
|
|
548f6c170a | ||
|
|
17c07960f9 | ||
|
|
c4d448641a | ||
|
|
77a9269828 | ||
|
|
9a16c89714 | ||
|
|
a8c781fd64 | ||
|
|
690b462577 | ||
|
|
25ac0c041f | ||
|
|
edef19c3fb | ||
|
|
d55d92cd00 | ||
|
|
d9253bf930 | ||
|
|
f5ce55bcb4 | ||
|
|
4480bd4359 | ||
|
|
e43b123c8a | ||
|
|
ed9bdd6ffa | ||
|
|
7b733e54ab | ||
|
|
edfaf6c0d1 | ||
|
|
f83ad951f6 | ||
|
|
22003c2c32 | ||
|
|
8376699f5e | ||
|
|
5c98a9a1ea | ||
|
|
7c17de7f09 | ||
|
|
a7e8d485f3 | ||
|
|
0957be6e76 | ||
|
|
4a91f77e62 | ||
|
|
a7437f7ca4 | ||
|
|
498945c401 | ||
|
|
7b17a08628 | ||
|
|
0b5c3927c9 | ||
|
|
881267c56d | ||
|
|
d7c0db9455 | ||
|
|
c1d14a86d9 | ||
|
|
a2a2a40d71 | ||
|
|
543660fcdf | ||
|
|
98a95002b2 | ||
|
|
c4a3d83abe | ||
|
|
7753b5de41 | ||
|
|
c1fe1eb925 | ||
|
|
3357317a1d | ||
|
|
a08f425ae7 | ||
|
|
5b780addba | ||
|
|
fc9f41839c | ||
|
|
5859944563 | ||
|
|
aaa6459e55 | ||
|
|
a898e1d86c | ||
|
|
4045f1ad3f | ||
|
|
e6214a3f73 | ||
|
|
686e8bf969 | ||
|
|
9214cb945f | ||
|
|
a79a2d9ef6 | ||
|
|
9608da9b96 | ||
|
|
93b3e624d8 | ||
|
|
15f515c1ba | ||
|
|
e255e8f334 | ||
|
|
16aa61f98e | ||
|
|
a939003b92 | ||
|
|
8f4873f2e2 | ||
|
|
a8b45a8b8b | ||
|
|
1a70281c76 | ||
|
|
f5f4ff6eda | ||
|
|
eb1b5ba0ea | ||
|
|
6732fc7c32 | ||
|
|
6ce6b84015 | ||
|
|
63d89aa906 | ||
|
|
8c212be86b | ||
|
|
89de56d2a2 | ||
|
|
d298025d7c | ||
|
|
a3287a0180 | ||
|
|
9adfd50c58 | ||
|
|
fda7c9e5b3 | ||
|
|
a8266c6d7b | ||
|
|
64a6b9677a | ||
|
|
20d3a1a7a5 | ||
|
|
4826c65042 | ||
|
|
7822ab08d3 | ||
|
|
41bd6d82ce | ||
|
|
6f8dd1d971 | ||
|
|
ddd0af5474 | ||
|
|
fbd2e06419 | ||
|
|
4442fb4341 | ||
|
|
35d1c61cb4 | ||
|
|
a183e153ef | ||
|
|
2fa78c3826 | ||
|
|
5ae1eab14c | ||
|
|
321c25bd7d | ||
|
|
f527c1ef73 | ||
|
|
0a5a1242f2 | ||
|
|
6946dc8e14 | ||
|
|
22b78779ca | ||
|
|
35607f5050 | ||
|
|
60af95ac06 | ||
|
|
46073e5f50 | ||
|
|
bf12abe2f1 | ||
|
|
1491b91829 | ||
|
|
63f669d7f8 | ||
|
|
d525204dda | ||
|
|
6eb31984fd | ||
|
|
9faeb0aa25 | ||
|
|
f6db3e76e9 | ||
|
|
9163d48f0a | ||
|
|
1454ee216c | ||
|
|
83ee2cda34 | ||
|
|
3c352b5752 | ||
|
|
60ee927e32 | ||
|
|
eb07a525eb | ||
|
|
6fc4231142 | ||
|
|
fa69736300 | ||
|
|
73a5e9d788 | ||
|
|
dc6f56e8a8 | ||
|
|
2c9da8507d | ||
|
|
1db354464f | ||
|
|
395a80e74b | ||
|
|
6550c7c827 | ||
|
|
c959866436 | ||
|
|
e3a1c041b6 | ||
|
|
e2324e26da | ||
|
|
f01e770c35 | ||
|
|
abb56ec014 | ||
|
|
0300c1b51e | ||
|
|
9896242333 | ||
|
|
e3812be97b | ||
|
|
e7ff0da4ba | ||
|
|
65261fa658 | ||
|
|
cb5b2e7d0f | ||
|
|
f58fd4b50d | ||
|
|
1a5f91a192 | ||
|
|
50cc26f297 | ||
|
|
c92613b19e | ||
|
|
7aacc8990b | ||
|
|
8e934789a1 | ||
|
|
d26e6b0615 | ||
|
|
a9c33de182 | ||
|
|
d0935f6e42 | ||
|
|
3e0337ab94 | ||
|
|
5cf43c48ef | ||
|
|
68567f4a14 | ||
|
|
d0d71bb4f9 | ||
|
|
ec7eff1697 | ||
|
|
1882c3c31e | ||
|
|
f21f5677e8 | ||
|
|
fc5a76d5e2 | ||
|
|
fbc34a1846 | ||
|
|
3ef5ea341b | ||
|
|
4195c41171 | ||
|
|
232cc3d916 | ||
|
|
1b4af30a49 | ||
|
|
39e0e0718d | ||
|
|
01d391ac91 | ||
|
|
85d70ba9dd | ||
|
|
3cfecbcc1f | ||
|
|
c6e9746ed6 | ||
|
|
d6961ecb4d | ||
|
|
02242eecf6 | ||
|
|
269456def3 | ||
|
|
086e379f2c | ||
|
|
38803a18ae | ||
|
|
3980d09bcd | ||
|
|
64d4847350 | ||
|
|
cf74e90c1d | ||
|
|
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 |
@@ -0,0 +1,5 @@
|
|||||||
|
# CodeGraph data files — local to each machine, not for committing.
|
||||||
|
# Ignore everything in .codegraph/ except this file itself, so transient
|
||||||
|
# files (the database, daemon.pid, sockets, logs) never show up in git.
|
||||||
|
*
|
||||||
|
!.gitignore
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
!/module
|
||||||
|
!/plugins
|
||||||
|
!/public
|
||||||
|
!/static
|
||||||
|
!/util
|
||||||
|
!/app.js
|
||||||
|
!/server.js
|
||||||
|
!/package.json
|
||||||
|
!/index.js
|
||||||
|
!/generateConfig.js
|
||||||
|
!/main.js
|
||||||
|
!/data
|
||||||
|
!/.env
|
||||||
|
!/pnpm-lock.yaml
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
|
||||||
|
# EditorConfig is awesome: http://EditorConfig.org
|
||||||
|
|
||||||
|
# top-most EditorConfig file
|
||||||
|
root = true
|
||||||
|
|
||||||
|
# Unix-style newlines with a newline ending every file
|
||||||
|
[*]
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
# Matches multiple files with brace expansion notation
|
||||||
|
# Set default charset
|
||||||
|
[*.{js,py}]
|
||||||
|
charset = utf-8
|
||||||
|
|
||||||
|
# 4 space indentation
|
||||||
|
[*.py]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
# Tab indentation (no size specified)
|
||||||
|
[Makefile]
|
||||||
|
indent_style = tab
|
||||||
|
|
||||||
|
# Indentation override for all JS under lib directory
|
||||||
|
[*.{js,ts}]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
# Matches the exact files either package.json or .travis.yml
|
||||||
|
[{package.json,.travis.yml}]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
### CORS资源共享设置
|
||||||
|
## 如果你需要配置, 请取消下面的注释并设置具体的域名
|
||||||
|
# CORS_ALLOW_ORIGIN = "*"
|
||||||
|
|
||||||
|
### 代理设置
|
||||||
|
## 启用反向代理
|
||||||
|
ENABLE_PROXY = false
|
||||||
|
## 代理配置
|
||||||
|
PROXY_URL = "https://your-proxy-url.com/?proxy="
|
||||||
|
|
||||||
|
### 随机IP设置
|
||||||
|
## 启用随机中国IP(默认关闭);启用后,所有请求默认使用随机中国IP,除非请求参数randomCNIP显式关闭
|
||||||
|
ENABLE_RANDOM_CN_IP = false
|
||||||
|
|
||||||
|
### UnblockNeteaseMusic 设置项
|
||||||
|
## 启用全局解灰, 无论是否调用参数都会使用解灰(不推荐开启)
|
||||||
|
ENABLE_GENERAL_UNBLOCK = false
|
||||||
|
## 歌曲启用无损音质
|
||||||
|
ENABLE_FLAC = true
|
||||||
|
## 启用无损音质时,是否选择音质最高的
|
||||||
|
SELECT_MAX_BR = false
|
||||||
|
## 严格按照配置音源设置顺序进行匹配
|
||||||
|
FOLLOW_SOURCE_ORDER = true
|
||||||
|
## Cookie设置项; 推荐在Vercel等平台进行环境变量的设置或者设置github secret
|
||||||
|
# 网易云 cookie
|
||||||
|
# 格式:MUSIC_U=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||||
|
# NETEASE_COOKIE = ""
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
custom: ["https://rewards.focalors.ltd"]
|
||||||
@@ -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,73 @@
|
|||||||
|
name: ✨ 接口添加请求
|
||||||
|
description: 希望添加新接口
|
||||||
|
title: "[Feature] "
|
||||||
|
labels: ["enhancement"]
|
||||||
|
assignees: []
|
||||||
|
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
感谢您提出新功能建议!请填写以下信息帮助我们理解您的需求。
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: terms
|
||||||
|
attributes:
|
||||||
|
label: 确认事项
|
||||||
|
description: |
|
||||||
|
在提交功能请求前,请确认以下事项
|
||||||
|
|
||||||
|
- 我已经搜索了现有的issues,确认这不是重复请求
|
||||||
|
- 我已经查看了项目文档
|
||||||
|
- 该项目符合该项目的目标和范围
|
||||||
|
- 我确认我提供的接口请求是合理且有意义的
|
||||||
|
|
||||||
|
options:
|
||||||
|
- label: 我已确认以上事项
|
||||||
|
required: true
|
||||||
|
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: proposed-solution
|
||||||
|
attributes:
|
||||||
|
label: 功能描述
|
||||||
|
description: 请详细描述您希望添加的功能,最好贴上对应客户端界面的截图
|
||||||
|
placeholder: |
|
||||||
|
请详细描述您期望的功能:
|
||||||
|
- 新的网易云音乐api 接口?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: priority
|
||||||
|
attributes:
|
||||||
|
label: 优先级
|
||||||
|
description: 您认为这个功能的优先级如何?
|
||||||
|
options:
|
||||||
|
- 低 (可有可无的改进)
|
||||||
|
- 中 (会显著改善使用体验)
|
||||||
|
- 高 (严重影响了我使用)
|
||||||
|
- 紧急 (阻塞了重要工作)
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: compatibility
|
||||||
|
attributes:
|
||||||
|
label: 兼容性考虑
|
||||||
|
description: 这个功能是否会影响现有代码的兼容性?
|
||||||
|
placeholder: |
|
||||||
|
- 是否是破坏性变更?
|
||||||
|
- 是否需要语法变更?
|
||||||
|
- 对性能的影响?
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: implementation
|
||||||
|
attributes:
|
||||||
|
label: 实现意愿
|
||||||
|
description: 您是否愿意参与这个功能的实现?
|
||||||
|
options:
|
||||||
|
- label: 我愿意提交PR来实现这个功能
|
||||||
|
- label: 我可以协助设计API接口
|
||||||
|
- label: 我可以提供测试用例
|
||||||
|
- label: 我可以帮助编写文档
|
||||||
@@ -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"
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
name: Build Artifacts
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch: # 手动触发
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||||
|
include:
|
||||||
|
- os: ubuntu-latest
|
||||||
|
platform: linux
|
||||||
|
target: node22-linux-x64
|
||||||
|
output: precompiled/app
|
||||||
|
- os: windows-latest
|
||||||
|
platform: win
|
||||||
|
target: node22-win-x64
|
||||||
|
output: precompiled/app.exe
|
||||||
|
- os: macos-latest
|
||||||
|
platform: macos
|
||||||
|
target: node22-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: '24'
|
||||||
|
|
||||||
|
- name: Setup pnpm
|
||||||
|
run: npm install -g pnpm@9
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Build for ${{ matrix.platform }}
|
||||||
|
run: |
|
||||||
|
npm run pkg${{ matrix.platform }}
|
||||||
|
env:
|
||||||
|
PKG_TARGET: ${{ matrix.target }}
|
||||||
|
|
||||||
|
- name: Upload artifacts
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
with:
|
||||||
|
name: app-${{ matrix.platform }}
|
||||||
|
path: ${{ matrix.output }}
|
||||||
|
if-no-files-found: error
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ci-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check:
|
||||||
|
name: Lint / Unit / Docs
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node-version: [22, 24]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- uses: pnpm/action-setup@v6
|
||||||
|
with:
|
||||||
|
version: 9
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v6
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node-version }}
|
||||||
|
cache: pnpm
|
||||||
|
|
||||||
|
# frozen-lockfile: 顺带校验 package.json 与 pnpm-lock.yaml 是否一致
|
||||||
|
- name: Install dependencies (frozen lockfile)
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Lint
|
||||||
|
run: pnpm lint
|
||||||
|
|
||||||
|
# 纯单测,不需要网络,稳定必过
|
||||||
|
- name: Unit tests
|
||||||
|
run: pnpm exec mocha -r intelli-espower-loader -t 60000 main.test.js --exit
|
||||||
|
|
||||||
|
- name: Docs format check
|
||||||
|
run: pnpm docs:check
|
||||||
|
|
||||||
|
integration:
|
||||||
|
# server.test.js 会启动真实服务器并请求真实网易云 API,
|
||||||
|
# 可能因上游风控/限流偶发失败,因此不阻塞合并
|
||||||
|
name: Integration tests (real NetEase API)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
continue-on-error: true
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- uses: pnpm/action-setup@v6
|
||||||
|
with:
|
||||||
|
version: 9
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v6
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
cache: pnpm
|
||||||
|
|
||||||
|
- name: Install dependencies (frozen lockfile)
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Run full test suite
|
||||||
|
run: pnpm test
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
name: Issue Management
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types: [opened, labeled]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
welcome-new-issues:
|
||||||
|
if: github.event.action == 'opened'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Welcome new issue
|
||||||
|
uses: peter-evans/create-or-update-comment@v5
|
||||||
|
with:
|
||||||
|
issue-number: ${{ github.event.issue.number }}
|
||||||
|
body: |
|
||||||
|
感谢您在该仓库提出问题!我们非常重视您的反馈,并希望尽快为您提供帮助。
|
||||||
|
|
||||||
|
如果您在一周左右的时间里没有收到我们的回复,请在下方留言,以便我们注意到。我们的大部分审核都是志愿者,有时难免会有疏漏。
|
||||||
|
|
||||||
|
你可能会想要尝试以下方法来获得更快的帮助:
|
||||||
|
- 📖 查看[项目文档](https://neteasecloudmusicapienhanced.js.org)
|
||||||
|
- 💬 加入[QQ交流群](https://qm.qq.com/q/TpeP9Uv2yk)
|
||||||
|
- 🔍 搜索[现有issues](https://github.com/neteasecloudmusicapienhanced/api-enhanced/issues) 看是否有类似问题
|
||||||
|
|
||||||
|
stale-issues:
|
||||||
|
if: github.event.action == 'opened'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Mark stale issues
|
||||||
|
uses: actions/stale@v11
|
||||||
|
with:
|
||||||
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
stale-issue-message: |
|
||||||
|
这个issue已经30天没有活动了。
|
||||||
|
|
||||||
|
如果这个问题仍然存在,请回复此评论说明当前状态。
|
||||||
|
否则,这个issue将在7天后自动关闭。
|
||||||
|
|
||||||
|
您可以随时重新打开已关闭的issue。
|
||||||
|
stale-pr-message: |
|
||||||
|
这个PR已经30天没有活动了。
|
||||||
|
|
||||||
|
如果您仍在处理这个PR,请回复此评论。
|
||||||
|
否则,这个PR将在7天后自动关闭。
|
||||||
|
days-before-stale: 30
|
||||||
|
days-before-close: 7
|
||||||
|
stale-issue-label: "stale"
|
||||||
|
stale-pr-label: "stale"
|
||||||
|
exempt-issue-labels: "pinned,enhancement,priority:high,ongoing,fatal"
|
||||||
|
exempt-pr-labels: "pinned,priority:high,ongoing,fatal"
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
name: opencode
|
||||||
|
|
||||||
|
on:
|
||||||
|
issue_comment:
|
||||||
|
types: [created]
|
||||||
|
pull_request_review_comment:
|
||||||
|
types: [created]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
issues: write
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
bot:
|
||||||
|
if: |
|
||||||
|
(contains(github.event.comment.body, ' /oc') ||
|
||||||
|
startsWith(github.event.comment.body, '/oc') ||
|
||||||
|
contains(github.event.comment.body, ' /opencode') ||
|
||||||
|
startsWith(github.event.comment.body, '/opencode')) &&
|
||||||
|
github.actor != 'opencode-agent[bot]' &&
|
||||||
|
github.actor != 'yukka-agent[bot]' &&
|
||||||
|
github.actor != 'github-actions[bot]'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
steps:
|
||||||
|
- name: Fetch config from CF Worker
|
||||||
|
id: cfg
|
||||||
|
uses: MoeFurina/pull-oc-config@v1
|
||||||
|
with:
|
||||||
|
url: https://bot-storage.interface.focalors.ltd/config
|
||||||
|
|
||||||
|
- name: Generate bot token
|
||||||
|
id: token
|
||||||
|
uses: actions/create-github-app-token@v3
|
||||||
|
with:
|
||||||
|
client-id: ${{ steps.cfg.outputs.app_id }}
|
||||||
|
private-key: ${{ steps.cfg.outputs.app_private_key }}
|
||||||
|
owner: ${{ github.repository_owner }}
|
||||||
|
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
# 需要持久化凭证: opencode action 内部会 git fetch origin <分支> 拉取 PR 代码
|
||||||
|
persist-credentials: true
|
||||||
|
token: ${{ steps.token.outputs.token }}
|
||||||
|
|
||||||
|
- name: Configure git identity
|
||||||
|
run: |
|
||||||
|
git config --global user.name "yukka-agent[bot]"
|
||||||
|
git config --global user.email "yukka-agent[bot]@users.noreply.github.com"
|
||||||
|
|
||||||
|
- name: Run opencode
|
||||||
|
uses: anomalyco/opencode/github@latest
|
||||||
|
env:
|
||||||
|
OPENCODE_API_KEY: ${{ steps.cfg.outputs.opencode_api_key }}
|
||||||
|
GITHUB_TOKEN: ${{ steps.token.outputs.token }}
|
||||||
|
OPENCODE_PERMISSION: '{"permission":{"*":"allow","question":"deny"}}'
|
||||||
|
with:
|
||||||
|
model: opencode/muse-spark-1.3-contributor-free
|
||||||
|
use_github_token: true
|
||||||
|
prompt: |
|
||||||
|
${{ steps.cfg.outputs.opencode_prompt }}
|
||||||
|
|
||||||
|
## 用户指令(来自触发评论,请严格按此执行)
|
||||||
|
${{ github.event.comment.body }}
|
||||||
|
|
||||||
|
## 用户要求Review时, 严格执行下面指令
|
||||||
|
${{ steps.cfg.outputs.pr_review_prompt }}
|
||||||
@@ -0,0 +1,315 @@
|
|||||||
|
name: Create Release on Version Change
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- package.json
|
||||||
|
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: release-${{ github.ref }}
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
detect:
|
||||||
|
name: Detect Version Change
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
outputs:
|
||||||
|
version: ${{ steps.version.outputs.value }}
|
||||||
|
should_release: ${{ steps.check.outputs.should_release }}
|
||||||
|
tag_exists: ${{ steps.tagcheck.outputs.exists }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Extract version from package.json
|
||||||
|
id: version
|
||||||
|
run: |
|
||||||
|
echo "value=$(jq -r .version package.json)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Check if version changed
|
||||||
|
id: check
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
|
||||||
|
echo "should_release=true" >> $GITHUB_OUTPUT
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if git diff "${{ github.event.before }}" "${{ github.sha }}" -- package.json | grep -q '"version"'; then
|
||||||
|
echo "should_release=true" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "should_release=false" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Check if tag already exists
|
||||||
|
id: tagcheck
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.version.outputs.value }}"
|
||||||
|
|
||||||
|
if git rev-parse "v$VERSION" >/dev/null 2>&1; then
|
||||||
|
echo "exists=true" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "exists=false" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
build:
|
||||||
|
name: Build ${{ matrix.platform }}
|
||||||
|
needs: detect
|
||||||
|
|
||||||
|
if: |
|
||||||
|
needs.detect.outputs.should_release == 'true' &&
|
||||||
|
needs.detect.outputs.tag_exists != 'true'
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- os: ubuntu-latest
|
||||||
|
platform: linux
|
||||||
|
target: node22-linux-x64
|
||||||
|
output: ncm-api-linux-x64
|
||||||
|
|
||||||
|
- os: windows-latest
|
||||||
|
platform: windows
|
||||||
|
target: node22-win-x64
|
||||||
|
output: ncm-api-win-x64.exe
|
||||||
|
|
||||||
|
- os: macos-latest
|
||||||
|
platform: macos
|
||||||
|
target: node22-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
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
name: Upstream Sync
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "0 4 * * *" # At 12PM UTC+8
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
sync_latest_from_upstream:
|
||||||
|
name: Sync latest commits from upstream repo
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ github.event.repository.fork }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
# Step 1: run a standard checkout action
|
||||||
|
- name: Checkout target repo
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
# Step 2: run the sync action
|
||||||
|
- name: Sync upstream changes
|
||||||
|
id: sync
|
||||||
|
uses: aormsby/[email protected]
|
||||||
|
with:
|
||||||
|
upstream_sync_repo: NeteaseCloudMusicApiEnhanced/api-enhanced
|
||||||
|
upstream_sync_branch: main
|
||||||
|
target_sync_branch: main
|
||||||
|
target_repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Sync check
|
||||||
|
if: failure()
|
||||||
|
run: |
|
||||||
|
echo "[Error] Due to a change in the workflow file of the upstream repository, GitHub has automatically suspended the scheduled automatic update. You need to manually sync your fork."
|
||||||
|
exit 1
|
||||||
+13
-1
@@ -1 +1,13 @@
|
|||||||
node_modules/
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
*.log
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
.history
|
||||||
|
examples/moddef.json
|
||||||
|
bin
|
||||||
|
anonymous_token
|
||||||
|
.vercel
|
||||||
|
.env
|
||||||
|
IFLOW.md
|
||||||
|
precompiled
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
node_modules
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"semi": false,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"singleQuote": true
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
language: node_js
|
||||||
|
|
||||||
|
node_js:
|
||||||
|
- 22
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# NeteaseCloudMusicApiEnhanced Agent 说明
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
- **包管理器**:开发与 CI 使用 `pnpm`,`Dockerfile` 也用 pnpm(`pnpm@9 --frozen-lockfile`,匹配 `pnpm-lock.yaml`)。仓库没有 `yarn.lock`,不要引入 yarn。
|
||||||
|
- **Node 版本**:`package.json` 的 `engines` 声明 `>=22`;CI/打包在 Node 22–24 上运行。现代 Node 均可。
|
||||||
|
- **环境变量**:`server.js` 调用了 `dotenv.config()`,本地 `.env` 会被自动加载;所有支持的变量见 `.env.prod.example`。
|
||||||
|
|
||||||
|
## 常用命令
|
||||||
|
- 安装依赖:`pnpm i`
|
||||||
|
- 启动服务:`pnpm start`(等价 `node app.js`);热重载开发:`pnpm dev`(nodemon)
|
||||||
|
- 跑测试:`pnpm test`(Mocha,超时 60s)
|
||||||
|
- Lint:`pnpm lint`;自动修复:`pnpm lint-fix`
|
||||||
|
- 文档格式化检查/修复:`pnpm docs:check` / `pnpm docs:format`
|
||||||
|
- 打包独立二进制:`pnpm pkgwin` / `pkglinux` / `pkgmacos`(@yao-pkg/pkg,node22 目标,`--no-bytecode` + `--public` 规避 node22 bytecode 回归)
|
||||||
|
|
||||||
|
## 目录内非路由文件
|
||||||
|
- `plugins/`——内部上传辅助(`upload.js`、`songUpload.js`),被 `module/*` require,本身不是路由。
|
||||||
|
- `module_example/`——接口模板与 `main.js` 库调用示例(含 `test.js` 演示 `login_cellphone`→`song_url` 链路),不是运行时代码。
|
||||||
|
- `examples/get_static_moddef.js`——用 `getModulesDefinitions(..., false)`(`doRequire=false`)把全部路由导出为 `moddef.json`。改路由注册逻辑时可跑它核对。
|
||||||
|
- `module_types/` 为空目录;类型声明在根目录 `interface.d.ts`(`types` 字段)。
|
||||||
|
|
||||||
|
## 架构
|
||||||
|
- `app.js`(也是 `bin`)——服务入口。先确保 `os.tmpdir()` 里存在 `anonymous_token`,执行 `generateConfig()` 刷新匿名 cookie 与 xeapi 公钥,再调用 `server.serveNcmApi()`。
|
||||||
|
- `server.js`——Express 工厂。`constructServer()` 自动扫描 `module/*.js`,每个文件注册一条路由(文件名 `_` 转 `/`,如 `album_new.js` → `/album/new`;特例 `daily_signin`/`fm_trash`/`personal_fm` 硬编码在 `server.js` 的 `special` 对象里)。`serveNcmApi()` 监听 `PORT`(默认 3000)/`HOST`。
|
||||||
|
- `main.js`——作为依赖被引入时的入口(`main` 字段)。把每个 `module/*` 导出为同名函数 `name(data)`,另导出 `server`、`serveNcmApi`、`getModulesDefinitions`。
|
||||||
|
- `module/*.js`——每个接口一个文件,标准写法:`module.exports = (query, request) => request(path, data, createOption(query))`。`createOption` 在 `util/option.js`,负责 crypto、cookie(回退到 `NETEASE_COOKIE`)、proxy、realIP/randomCNIP、headers、timeout。
|
||||||
|
- `util/request.js`——唯一的对外 HTTP 层(axios)。按 `crypto`(`api`/`eapi`/`weapi`/`linuxapi`/`xeapi`)加密并设置 IP 头;在 require 时同步读取 `os.tmpdir()` 里的 `anonymous_token` 与 `xeapi_public_key`。
|
||||||
|
- `util/config.json`——运行时配置:网易域名 + `APP_CONF.encrypt: true`(默认走 eapi 加密)。已被 git 跟踪,改动会改变全局默认行为。
|
||||||
|
- `index.js` / `index.mjs`——`require('./app.js')` 的薄包装,供 Vercel(`vercel.json`)和 ESM 导入使用。
|
||||||
|
- `server.js` 里有一段非显而易见的逻辑:环境变量 `ENABLE_GENERAL_UNBLOCK=true` 时,`/song/url/v1` 的响应会被自动解灰(走 `@neteasecloudmusicapienhanced/unblockmusic-utils`);另外 `query.noCookie` 为真时不向响应写 `Set-Cookie`。
|
||||||
|
|
||||||
|
## 新增/修改接口
|
||||||
|
- 新建 `module/xxx.js` 会自动挂载路由,无需注册;**文件名即路由**。
|
||||||
|
- 照抄同目录模块的写法(选对 `crypto`),用 `createOption(query)` 生成请求选项。
|
||||||
|
- 改文件名/路由会破坏已有客户端,尽量保持旧路径兼容。
|
||||||
|
|
||||||
|
## 测试
|
||||||
|
- `pnpm test` 跑 `server.test.js` + `main.test.js`。`server.test.js` 在 `before()` 里启动真实服务器,`test/*.test.js` 全部请求**真实网易云 API**——必须联网,且可能因上游风控/限流偶发失败。`main.test.js` 是纯单测,不需要网络。
|
||||||
|
- **CI 门禁只看稳定项**:`ci-check.yml` 在 Node 18/22/24 上跑 `pnpm lint` + `main.test.js` + `pnpm docs:check`;完整集成测试(`server.test.js`)是 `continue-on-error`,不阻塞合并。本地跑全量测试失败先怀疑上游风控。
|
||||||
|
- 测试使用 `power-assert`(经 `intelli-espower-loader`),普通 `assert` 写法也会输出详细 diff。
|
||||||
|
- 只跑单个用例:`pnpm exec mocha -r intelli-espower-loader -t 60000 --grep "<describe/it 名字>" server.test.js main.test.js --exit`
|
||||||
|
- 只跑离线单测:`pnpm exec mocha -r intelli-espower-loader -t 60000 main.test.js --exit`
|
||||||
|
|
||||||
|
## 坑与注意
|
||||||
|
- **改 `package.json` 的 `version` 会触发自动发布**:`release-on-version-change.yml` 按 paths 过滤——只要 push 到 `main` 时 `package.json` 有改动且 version 变了,就自动打 tag + GitHub Release(`pkg` 三平台二进制)、推送 Docker 镜像(Docker Hub + GHCR 多架构)、`pnpm publish` 到 npm;tag 已存在则跳过。别顺手改版本号,也别往 `package.json` 里写无关改动凑提交。
|
||||||
|
- **没有实际 git hooks**:`package.json` 里配了 `lint-staged` 和 husky,但 `.husky/` 下只有 `_` 脚手架目录、没有真正的 hook 文件,commit 时不会自动跑任何检查,自己记得 `pnpm lint-fix`。
|
||||||
|
- **代理环境变量已失效**:README 里关于 `http_proxy`/`https_proxy` 的警告来自旧 `request` 库时代;现在 `util/request.js` 用 axios + 自定义 keep-alive agent,且显式 `proxy: false`,环境变量代理不会生效。按请求走 `query.proxy` 参数(支持 PAC 和 http 隧道)。
|
||||||
|
- **启动令牌在系统临时目录**:`anonymous_token`、`xeapi_public_key` 存放在 `os.tmpdir()`,`util/request.js` 在 require 时同步读取。文件过期或被清空就重启服务(或调用 `generateConfig()`);首次启动先写空文件再刷新。
|
||||||
|
- **`docs:check` 校验的是 `public/docs/home.md`(docsify 站点)**,不是 README;脚本默认只处理该文件,换行会被统一为 `\n`、连续空行压缩到 2 行,PR 改动它后记得跑 `pnpm docs:format`。
|
||||||
|
- **ESLint 9 flat config**:`eslint.config.js`,风格由 `eslint-plugin-prettier` 强制(2 空格缩进、单引号、分号、`endOfLine: auto`),并 `globalIgnores(['**/public/'])`——`public/` 下的前端页面不参与 lint。
|
||||||
|
- **`pnpm-workspace.yaml` 是单包工作区**(`packages: ['.']`),并带 `allowBuilds`(core-js/es5-ext)与 `minimumReleaseAgeExclude` 白名单;改依赖安装行为时先看它。
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
FROM node:lts-alpine
|
||||||
|
|
||||||
|
RUN apk add --no-cache tini
|
||||||
|
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
|
RUN npm install -g pnpm@9
|
||||||
|
|
||||||
|
USER node
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY --chown=node:node . ./
|
||||||
|
|
||||||
|
# --prod 模式下 husky 不会被安装,prepare 脚本会因找不到 husky 而失败,
|
||||||
|
# 故显式跳过生命周期脚本;本项目生产运行也不依赖任何 postinstall 步骤。
|
||||||
|
RUN pnpm install --frozen-lockfile --prod --ignore-scripts
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
CMD [ "/sbin/tini", "--", "node", "app.js" ]
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2013-2022 Binaryify
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
@@ -0,0 +1,253 @@
|
|||||||
|
<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, 支持丰富的音乐相关接口,适合自建服务、二次开发和多平台部署
|
||||||
|
|
||||||
|
> [!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 22 及以上
|
||||||
|
- 推荐使用 pnpm 进行依赖管理
|
||||||
|
|
||||||
|
### 安装
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/neteasecloudmusicapienhanced/api-enhanced.git
|
||||||
|
cd api-enhanced
|
||||||
|
pnpm i
|
||||||
|
```
|
||||||
|
|
||||||
|
### 启动服务
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 默认端口 3000
|
||||||
|
node app.js
|
||||||
|
|
||||||
|
# 指定端口(如 4000)
|
||||||
|
PORT=4000 node app.js # Mac/Linux
|
||||||
|
set PORT=4000 && node app.js # Windows
|
||||||
|
```
|
||||||
|
|
||||||
|
### 重要提示
|
||||||
|
|
||||||
|
- 调用前请务必阅读文档的 [调用前须知](https://neteasecloudmusicapienhanced.js.org/#/?id=%e8%b0%83%e7%94%a8%e5%89%8d%e9%a1%bb%e7%9f%a5) 部分。
|
||||||
|
- 推荐将敏感信息(如 cookie)通过部署平台的环境变量进行配置。
|
||||||
|
|
||||||
|
## 在线体验与文档
|
||||||
|
|
||||||
|
- [在线文档](https://neteasecloudmusicapienhanced.js.org/)
|
||||||
|
- [NPM 包地址](https://www.npmjs.com/package/@neteasecloudmusicapienhanced/api)
|
||||||
|
- [Dockerhub 主页](https://hub.docker.com/r/moefurina/ncm-api)
|
||||||
|
|
||||||
|
## 常见部署方式
|
||||||
|
|
||||||
|
- 推荐参考[在线文档](https://neteasecloudmusicapienhanced.js.org/)
|
||||||
|
|
||||||
|
## 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 本项目
|
||||||
|
2. 在 Vercel 官网新建项目,导入 fork 的仓库
|
||||||
|
3. 直接 Deploy
|
||||||
|
|
||||||
|
## 腾讯云 Serverless 部署
|
||||||
|
|
||||||
|
1. fork 本项目
|
||||||
|
2. 在腾讯云 serverless 控制台新建 Web 应用,选择 Express 框架
|
||||||
|
3. 代码仓库选择 fork 的项目,启动文件填写:
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
export PORT=9000
|
||||||
|
/var/lang/node22/bin/node app.js
|
||||||
|
```
|
||||||
|
4. 完成部署后,访问 API 网关的 URL 即可
|
||||||
|
|
||||||
|
## Node.js 方式调用
|
||||||
|
|
||||||
|
支持直接在 Node.js 项目中引入和调用,返回 Promise:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const {
|
||||||
|
login_cellphone,
|
||||||
|
user_cloud,
|
||||||
|
} = require('@neteasecloudmusicapienhanced/api')
|
||||||
|
async function main() {
|
||||||
|
const result = await login_cellphone({ phone: '手机号', password: '密码' })
|
||||||
|
console.log(result)
|
||||||
|
const result2 = await user_cloud({ cookie: result.body.cookie })
|
||||||
|
console.log(result2.body)
|
||||||
|
}
|
||||||
|
main()
|
||||||
|
```
|
||||||
|
|
||||||
|
## TypeScript 支持
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { banner } from '@neteasecloudmusicapienhanced/api'
|
||||||
|
banner({ type: 0 }).then((res) => console.log(res))
|
||||||
|
```
|
||||||
|
|
||||||
|
## 单元测试
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm test
|
||||||
|
```
|
||||||
|
|
||||||
|
## 主要功能特性
|
||||||
|
|
||||||
|
- 登录/注册/验证码
|
||||||
|
- 用户信息、歌单、动态、播放记录
|
||||||
|
- 歌曲、专辑、歌手、MV、歌词、评论、排行榜
|
||||||
|
- 搜索、推荐、私人 FM、签到、云盘
|
||||||
|
- 歌曲解锁(解灰)
|
||||||
|
|
||||||
|
## 贡献与社区
|
||||||
|
|
||||||
|
- 欢迎提交 PR、Issue 参与维护
|
||||||
|
- 要贡献新接口, 请参考[这篇文章](https://www.focalors.ltd/post/how-to-contribute-ncm-api)
|
||||||
|
|
||||||
|
### 致谢
|
||||||
|
|
||||||
|
原作者 [Binaryify/NeteaseCloudMusicApi](https://github.com/binaryify/NeteaseCloudMusicApi) 项目为本项目基础 (该项目在`npmjs`网站上仍持续维护, 但 github 仓库已不再更新)
|
||||||
|
|
||||||
|
感谢大佬们为逆向eapi, weapi, xeapi等加密算法所做的贡献
|
||||||
|
|
||||||
|
项目参考:
|
||||||
|
|
||||||
|
[trazyn/ieaseMusic](https://github.com/trazyn/ieaseMusic)
|
||||||
|
|
||||||
|
[listen1/listen1_chrome_extension](https://github.com/listen1/listen1_chrome_extension)
|
||||||
|
|
||||||
|
[chaunsin/netease-cloud-music](https://github.com/chaunsin/netease-cloud-music)
|
||||||
|
|
||||||
|
[folltoshe/netease-report-listen-song](https://github.com/folltoshe/netease-report-listen-song)
|
||||||
|
|
||||||
|
### SDK 生态
|
||||||
|
|
||||||
|
| 语言 | 作者 | 地址 | 类型 |
|
||||||
|
|--------|---------------------------------------------|------------------------------------------------------------------------------------------|-----|
|
||||||
|
| Java | [JackuXL](https://github.com/JackuXL) | [NeteaseCloudMusicApi-SDK](https://github.com/JackuXL/NeteaseCloudMusicApi-SDK) | 第三方 |
|
||||||
|
| Java | [1015770492](https://github.com/1015770492) | [yumbo-music-utils](https://github.com/1015770492/yumbo-music-utils) | 第三方 |
|
||||||
|
| Python | [盧瞳](https://github.com/2061360308) | [NeteaseCloudMusic_PythonSDK](https://github.com/2061360308/NeteaseCloudMusic_PythonSDK) | 第三方 |
|
||||||
|
| Swift | [Lincb522](https://github.com/Lincb522) | [NeteaseCloudMusicApi-Swift](https://github.com/Lincb522/NeteaseCloudMusicAPI-Swift) | 第三方 |
|
||||||
|
| Rust | [SPlayer-Dev](https://github.com/splayer-dev) | [ncm-api-rs](https://github.com/SPlayer-Dev/ncm-api-rs) | 第三方 |
|
||||||
|
|
||||||
|
|
||||||
|
### 依赖此项目的优秀开源项目
|
||||||
|
|
||||||
|
- [qier2222/YesPlayMusic](https://github.com/qier222/YesPlayMusic)
|
||||||
|
|
||||||
|
- [MaigoLabs/amaoke.app](https://github.com/MaigoLabs/amaoke.app)
|
||||||
|
|
||||||
|
- [Yueby/music-together](https://github.com/Yueby/music-together)
|
||||||
|
|
||||||
|
- [chthollyphlie/folia-major](https://github.com/chthollyphile/folia-major)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
[MIT License](https://github.com/MoeFurina/NeteaseCloudMusicApiEnhanced/blob/main/LICENSE)
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
## 这是什么
|
|
||||||
这是网易云音乐api enhanced的文档页面
|
|
||||||
|
|
||||||
## [前往](https://docs-neteasecloudmusicapi.focalors.ltd)
|
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
const fs = require('fs')
|
||||||
|
const path = require('path')
|
||||||
|
const tmpPath = require('os').tmpdir()
|
||||||
|
|
||||||
|
// engines 字段只在 npm/pnpm 安装时生效,直接 node app.js 不会被拦截,
|
||||||
|
// 这里给出友好的最低版本检查(>=22,与 package.json engines 保持一致)
|
||||||
|
const [major] = process.versions.node.split('.').map(Number)
|
||||||
|
if (major < 22) {
|
||||||
|
console.error(
|
||||||
|
`[${new Date().toLocaleString()}] Node.js >= 22 is required (current: ${process.versions.node}). ` +
|
||||||
|
'请升级 Node.js 后重试 / Please upgrade Node.js to v22 or newer.',
|
||||||
|
)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function start() {
|
||||||
|
// 检测是否存在 anonymous_token 文件,没有则生成
|
||||||
|
if (!fs.existsSync(path.resolve(tmpPath, 'anonymous_token'))) {
|
||||||
|
fs.writeFileSync(path.resolve(tmpPath, 'anonymous_token'), '', 'utf-8')
|
||||||
|
}
|
||||||
|
// 启动时更新anonymous_token
|
||||||
|
const generateConfig = require('./generateConfig')
|
||||||
|
await generateConfig()
|
||||||
|
require('./server').serveNcmApi({
|
||||||
|
checkVersion: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
start()
|
||||||
File diff suppressed because it is too large
Load Diff
+24641
File diff suppressed because it is too large
Load Diff
@@ -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'),
|
||||||
|
},
|
||||||
|
])
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
const fsPromises = require('fs/promises')
|
||||||
|
const path = require('path')
|
||||||
|
const server = require('../server')
|
||||||
|
const logger = require('../util/logger.js')
|
||||||
|
|
||||||
|
const exportFile = path.join(__dirname, 'moddef.json')
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const def = await server.getModulesDefinitions(
|
||||||
|
path.join(__dirname, '..', 'module'),
|
||||||
|
{
|
||||||
|
'daily_signin.js': '/daily_signin',
|
||||||
|
'fm_trash.js': '/fm_trash',
|
||||||
|
'personal_fm.js': '/personal_fm',
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
|
||||||
|
fsPromises.writeFile(exportFile, JSON.stringify(def, null, 4))
|
||||||
|
logger.info(`👍 Get your own definition at: ${exportFile}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 14 KiB |
@@ -0,0 +1,55 @@
|
|||||||
|
const fs = require('fs')
|
||||||
|
const path = require('path')
|
||||||
|
const { register_anonimous } = require('./main')
|
||||||
|
const { cookieToJson, generateRandomChineseIP } = require('./util/index')
|
||||||
|
const { getXeapiPublicKey } = require('./util/xeapiKey')
|
||||||
|
const { loadNeapiKey } = require('./util/neapiKey')
|
||||||
|
const registerNeapikey = require('./module/register_neapikey')
|
||||||
|
const tmpPath = require('os').tmpdir()
|
||||||
|
|
||||||
|
async function generateConfig() {
|
||||||
|
global.cnIp = generateRandomChineseIP()
|
||||||
|
try {
|
||||||
|
const res = await register_anonimous()
|
||||||
|
const cookie = res.body.cookie
|
||||||
|
if (cookie) {
|
||||||
|
const cookieObj = cookieToJson(cookie)
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.resolve(tmpPath, 'anonymous_token'),
|
||||||
|
cookieObj.MUSIC_A,
|
||||||
|
'utf-8',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
let currentPublicKey = {}
|
||||||
|
try {
|
||||||
|
currentPublicKey = JSON.parse(
|
||||||
|
fs.readFileSync(path.resolve(tmpPath, 'xeapi_public_key'), 'utf-8'),
|
||||||
|
)
|
||||||
|
} catch (_) {}
|
||||||
|
const publicKey = await getXeapiPublicKey(currentPublicKey, global.deviceId)
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.resolve(tmpPath, 'xeapi_public_key'),
|
||||||
|
JSON.stringify(publicKey),
|
||||||
|
'utf-8',
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const current = loadNeapiKey()
|
||||||
|
await registerNeapikey(
|
||||||
|
{
|
||||||
|
version: current ? current.version : 0,
|
||||||
|
behavior: current ? 'update' : 'open',
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
module.exports = generateConfig
|
||||||
Vendored
+2917
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,63 @@
|
|||||||
|
const fs = require('fs')
|
||||||
|
const path = require('path')
|
||||||
|
const tmpPath = require('os').tmpdir()
|
||||||
|
const { cookieToJson } = require('./util')
|
||||||
|
|
||||||
|
const anonymousTokenPath = path.resolve(tmpPath, 'anonymous_token')
|
||||||
|
if (!fs.existsSync(anonymousTokenPath)) {
|
||||||
|
fs.writeFileSync(anonymousTokenPath, '', 'utf-8')
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @type {Record<string, any>} */
|
||||||
|
let obj = {}
|
||||||
|
|
||||||
|
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")}
|
||||||
|
*/
|
||||||
|
module.exports = {
|
||||||
|
get server() {
|
||||||
|
if (!serverModule) {
|
||||||
|
serverModule = require('./server')
|
||||||
|
}
|
||||||
|
return serverModule
|
||||||
|
},
|
||||||
|
...obj,
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(module.exports, require('./server'))
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
const assert = require('assert')
|
||||||
|
const main = require('./main')
|
||||||
|
|
||||||
|
describe('methods in server.js', () => {
|
||||||
|
it('has serveNcmApi', () => {
|
||||||
|
assert.strictEqual(typeof main.serveNcmApi, 'function')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('has getModulesDefinitions', () => {
|
||||||
|
assert.strictEqual(typeof main.getModulesDefinitions, 'function')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('methods in module', () => {
|
||||||
|
it('has activate_init_profile', () => {
|
||||||
|
assert.strictEqual(typeof main.activate_init_profile, 'function')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
// 初始化名字
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
nickname: query.nickname,
|
||||||
|
}
|
||||||
|
return request(`/api/activate/initProfile`, data, createOption(query))
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
// 获取广告
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
|
||||||
|
module.exports = async (query, request) => {
|
||||||
|
const data = {
|
||||||
|
type_ids: query.type_ids || '["400002_0"]',
|
||||||
|
}
|
||||||
|
|
||||||
|
const option = createOption(query, 'xeapi', 'v3')
|
||||||
|
|
||||||
|
const res = await request(`/api/ad/get`, data, option)
|
||||||
|
const raw = res.body
|
||||||
|
|
||||||
|
// 提取广告中的 req_id
|
||||||
|
let reqId = ''
|
||||||
|
try {
|
||||||
|
if (raw?.ads) {
|
||||||
|
const ad = Object.values(raw.ads)[0]
|
||||||
|
// 逆向 v9.5.61:客户端从 ad.adExtMap["req_id"] 取 reqUid
|
||||||
|
if (ad?.adExtMap) {
|
||||||
|
if (typeof ad.adExtMap === 'string') {
|
||||||
|
try {
|
||||||
|
reqId = JSON.parse(ad.adExtMap).req_id || ''
|
||||||
|
} catch (_) {}
|
||||||
|
} else {
|
||||||
|
reqId = ad.adExtMap.req_id || ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 兜底:adLogId.requestId / ad.reqId / extJson.contextInfo.req_id
|
||||||
|
if (!reqId && ad?.adLogId?.requestId) reqId = ad.adLogId.requestId
|
||||||
|
if (!reqId && ad?.reqId) reqId = ad.reqId
|
||||||
|
if (!reqId && ad?.extJson) {
|
||||||
|
try {
|
||||||
|
const ext = JSON.parse(ad.extJson)
|
||||||
|
reqId = ext?.contextInfo?.req_id || ''
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
code: 200,
|
||||||
|
ads: raw?.ads || null,
|
||||||
|
message: raw?.message || null,
|
||||||
|
extra: { reqId },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
// 获取免费听时长状态
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
entrance: 'FREE_LISTEN_RN',
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
`/api/ad/homepage/free/tab/extend/v2`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'xeapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,252 @@
|
|||||||
|
// 看广告免费听歌 - 领取免费听权益
|
||||||
|
// 请求流程(基于逆向网易云音乐 v9.5.61 原生 Kotlin 源码,classes5/18/19.dex):
|
||||||
|
// 1. 从广告平台拉广告 → 用户看完/点击广告 → 获取 ad 对象的 extJson.contextInfo.req_id 作为 reqUid
|
||||||
|
// 2. 调用本接口传入 reqUid 及相关权益参数,领取权益
|
||||||
|
// 3. 服务器返回 gainFlag 等标识,用于展示领取结果
|
||||||
|
//
|
||||||
|
// 调用链(逆向还原):
|
||||||
|
// AdDSLIncentiveVideoRightsHelper.requestRightsGainInner
|
||||||
|
// → AdDSLUtils.requestRightGain(ad, exposeTime, clickTime, creativeType, cb)
|
||||||
|
// → 构造 ListeningRightRequestParams(21 字段)→ JSON.stringify
|
||||||
|
// → body = { "reqParam": "..." } → POST /api/ad/listening/rights/gain
|
||||||
|
// (注意:客户端 AdDSLIncentiveVideoRightsHelper 写死 creativeType=36)
|
||||||
|
//
|
||||||
|
// 接口不只领取听歌时长,还包含"看视频得云贝"等权益:
|
||||||
|
// rightsGainMethod=6 (LAXIN_EXPOSE_OR_DOWNLOAD 拉新曝光/下载分段权益) 时,
|
||||||
|
// 弹窗 AdLaxinSegmentedGuideDialog 展示 "获得X云贝"(2000 云贝等),
|
||||||
|
// 权益数值由广告下发的 AdLaxinSegmentedRightsGuidePopup.rightsValue 决定,
|
||||||
|
// extraRightsType 作为权益类型随本接口上传。
|
||||||
|
//
|
||||||
|
// rightsGainMethod 枚举(h80/a):
|
||||||
|
// 1=EXPOSE 曝光, 2=EXPOSE_CLICK 曝光+点击, 3=EXPOSE_DOWNLOAD 曝光+下载,
|
||||||
|
// 4=CLICK_STAY 点击+停留, 5=EXPOSURE_OR_CLICK_STAY 曝光或点击停留,
|
||||||
|
// 6=LAXIN_EXPOSE_OR_DOWNLOAD 拉新曝光/下载分段权益
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
const adGet = require('./ad_get.js')
|
||||||
|
|
||||||
|
// 安全 JSON 解析(字符串字段可能是 JSON 文本)
|
||||||
|
function safeParse(str, fallback) {
|
||||||
|
if (typeof str !== 'string') return fallback
|
||||||
|
try {
|
||||||
|
return JSON.parse(str)
|
||||||
|
} catch (_) {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = async (query, request) => {
|
||||||
|
const time = Date.now()
|
||||||
|
|
||||||
|
// 从广告对象自动补齐请求字段。
|
||||||
|
// 实测 v9.5.61 真实 API 返回(/ad/get)确认的字段路径:
|
||||||
|
// reqUid = ad.extJson.contextInfo.req_id(真实响应中唯一存在)
|
||||||
|
// contextInfo = ad.extJson.contextInfo(完整对象序列化)
|
||||||
|
// generalRightsInfo = ad.generalRightsInfo(字符串 JSON)
|
||||||
|
// creativeType = ad.creativeType
|
||||||
|
// rightsGainMethod / extraRightsType / rightsGainDuration / rightsGainType /
|
||||||
|
// extraRightsGainMethod / extraRightsGainDuration / nextRightsGainDuration /
|
||||||
|
// rightsExtJson / source / rightsUpperLimit / qualified
|
||||||
|
// = ad.generalRightsInfo(字符串)解析后取值
|
||||||
|
// (逆向类字段 adExtMap/adLogId/listeningRightHintInfo 真实响应中不存在,仅兜底)
|
||||||
|
// sniffTime = method==3 或 6 时 currentTimeMillis
|
||||||
|
let reqUid = query.reqUid || ''
|
||||||
|
let contextInfo = query.contextInfo
|
||||||
|
let creativeType = query.creativeType
|
||||||
|
let generalRightsInfo = query.generalRightsInfo
|
||||||
|
|
||||||
|
// 广告 hint 配置:仅当调用方未显式传参时用广告下发值兜底
|
||||||
|
const hint = {}
|
||||||
|
|
||||||
|
if (!reqUid || contextInfo === undefined || creativeType === undefined) {
|
||||||
|
try {
|
||||||
|
const adRes = await adGet(
|
||||||
|
{ ...query, type_ids: query.type_ids || '["400002_0"]' },
|
||||||
|
request,
|
||||||
|
)
|
||||||
|
const ad = Object.values(adRes?.body?.ads || {})[0]
|
||||||
|
if (!ad) throw new Error('ads 为空(未登录或广告位无广告)')
|
||||||
|
|
||||||
|
// reqUid:真实 API 返回中 req_id 在 ad.extJson.contextInfo.req_id(实测 v9.5.61)
|
||||||
|
// (逆向类字段 adExtMap/adLogId 在真实响应中不存在,仅作兜底)
|
||||||
|
if (!reqUid) {
|
||||||
|
const ext =
|
||||||
|
typeof ad?.extJson === 'string'
|
||||||
|
? safeParse(ad.extJson, {})
|
||||||
|
: ad?.extJson || {}
|
||||||
|
const extMap =
|
||||||
|
typeof ad?.adExtMap === 'string'
|
||||||
|
? safeParse(ad.adExtMap, {})
|
||||||
|
: ad?.adExtMap
|
||||||
|
reqUid =
|
||||||
|
ext?.contextInfo?.req_id ||
|
||||||
|
extMap?.req_id ||
|
||||||
|
ad?.adLogId?.requestId ||
|
||||||
|
ad?.reqId ||
|
||||||
|
adRes?.body?.extra?.reqId ||
|
||||||
|
''
|
||||||
|
}
|
||||||
|
// contextInfo:真实 API 返回中在 ad.extJson.contextInfo(实测 v9.5.61)
|
||||||
|
if (contextInfo === undefined) {
|
||||||
|
const ext =
|
||||||
|
typeof ad?.extJson === 'string'
|
||||||
|
? safeParse(ad.extJson, {})
|
||||||
|
: ad?.extJson || {}
|
||||||
|
const ci =
|
||||||
|
ext?.contextInfo || ad?.adLogId?.contextInfo || ad?.showContext
|
||||||
|
if (ci) {
|
||||||
|
contextInfo = typeof ci === 'string' ? ci : JSON.stringify(ci)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (creativeType === undefined && ad?.creativeType !== undefined) {
|
||||||
|
creativeType = ad.creativeType
|
||||||
|
}
|
||||||
|
if (generalRightsInfo === undefined && ad?.generalRightsInfo) {
|
||||||
|
generalRightsInfo =
|
||||||
|
typeof ad.generalRightsInfo === 'string'
|
||||||
|
? ad.generalRightsInfo
|
||||||
|
: JSON.stringify(ad.generalRightsInfo)
|
||||||
|
}
|
||||||
|
// 缓存广告下发的领取配置(后续用于兜底)
|
||||||
|
// 实测 v9.5.61:rightsGainMethod/rightsUpperLimit/qualified 等在
|
||||||
|
// ad.generalRightsInfo(字符串)里,ad.listeningRightHintInfo 真实响应中不存在
|
||||||
|
const hintCfg =
|
||||||
|
typeof ad?.listeningRightHintInfo === 'string'
|
||||||
|
? safeParse(ad.listeningRightHintInfo, {})
|
||||||
|
: ad?.listeningRightHintInfo || {}
|
||||||
|
const gri =
|
||||||
|
typeof ad?.generalRightsInfo === 'string'
|
||||||
|
? safeParse(ad.generalRightsInfo, {})
|
||||||
|
: ad?.generalRightsInfo || {}
|
||||||
|
Object.assign(hint, hintCfg, gri)
|
||||||
|
console.log(`自动获取 reqUid: ${reqUid}`)
|
||||||
|
} catch (e) {
|
||||||
|
// 获取广告失败,后续请求会因缺少 reqUid 被拒绝
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const rightsGainMethod = query.rightsGainMethod
|
||||||
|
? parseInt(query.rightsGainMethod)
|
||||||
|
: hint.rightsGainMethod || 2
|
||||||
|
|
||||||
|
const rightsParam = {
|
||||||
|
// 必填: 广告请求 ID,自动从 ad_get 获取
|
||||||
|
reqUid,
|
||||||
|
|
||||||
|
// 曝光时间戳
|
||||||
|
exposureTime: query.exposureTime ? parseInt(query.exposureTime) : time,
|
||||||
|
|
||||||
|
// 当前登录用户 ID(原版客户端从 Profile.getUserId() 获取)
|
||||||
|
userId: query.uid ? parseInt(query.uid) : undefined,
|
||||||
|
|
||||||
|
// 点击时间戳
|
||||||
|
clickTime: query.clickTime ? parseInt(query.clickTime) : time,
|
||||||
|
|
||||||
|
// 额外权益类型(拉新分段权益等,决定发放云贝/时长/下载,来自广告配置)
|
||||||
|
extraRightsType: query.extraRightsType
|
||||||
|
? parseInt(query.extraRightsType)
|
||||||
|
: hint.extraRightsType !== undefined
|
||||||
|
? parseInt(hint.extraRightsType)
|
||||||
|
: undefined,
|
||||||
|
|
||||||
|
// 是否连续播放(默认 false)
|
||||||
|
playContinuously: query.playContinuously ? true : false,
|
||||||
|
|
||||||
|
// 来源标识(原版从 ad.listeningRightHintInfo.source 读取)
|
||||||
|
source: query.source
|
||||||
|
? parseInt(query.source)
|
||||||
|
: hint.source !== undefined
|
||||||
|
? parseInt(hint.source)
|
||||||
|
: undefined,
|
||||||
|
|
||||||
|
// 广告创意类型(激励视频场景=36,优先取 query / 广告对象)
|
||||||
|
creativeType: creativeType !== undefined ? parseInt(creativeType) : 36,
|
||||||
|
|
||||||
|
// 权益领取方式(1~6 枚举,见文件头注释)
|
||||||
|
rightsGainMethod,
|
||||||
|
|
||||||
|
// 权益扩展方式与时长
|
||||||
|
extraRightsGainMethod: query.extraRightsGainMethod
|
||||||
|
? parseInt(query.extraRightsGainMethod)
|
||||||
|
: hint.extraRightsGainMethod !== undefined
|
||||||
|
? parseInt(hint.extraRightsGainMethod)
|
||||||
|
: undefined,
|
||||||
|
extraRightsGainDuration: query.extraRightsGainDuration
|
||||||
|
? parseInt(query.extraRightsGainDuration)
|
||||||
|
: hint.extraRightsGainDuration !== undefined
|
||||||
|
? parseInt(hint.extraRightsGainDuration)
|
||||||
|
: undefined,
|
||||||
|
nextRightsGainDuration: query.nextRightsGainDuration
|
||||||
|
? parseInt(query.nextRightsGainDuration)
|
||||||
|
: hint.nextRightsGainDuration !== undefined
|
||||||
|
? parseInt(hint.nextRightsGainDuration)
|
||||||
|
: undefined,
|
||||||
|
|
||||||
|
// 权益类型(含 RIGHTS_GAIN_TYPE_CURRENT_DAY 等取值)
|
||||||
|
rightsGainType: query.rightsGainType
|
||||||
|
? parseInt(query.rightsGainType)
|
||||||
|
: hint.rightsGainType !== undefined
|
||||||
|
? parseInt(hint.rightsGainType)
|
||||||
|
: undefined,
|
||||||
|
|
||||||
|
// 权益时长
|
||||||
|
rightsGainDuration: query.rightsGainDuration
|
||||||
|
? parseInt(query.rightsGainDuration)
|
||||||
|
: hint.rightsGainDuration !== undefined
|
||||||
|
? parseInt(hint.rightsGainDuration)
|
||||||
|
: undefined,
|
||||||
|
|
||||||
|
// 领取步骤(UNGAIN/GAINING/GAIN_FINISHED 对应的值)
|
||||||
|
gainMethodStep: query.gainMethodStep
|
||||||
|
? parseInt(query.gainMethodStep)
|
||||||
|
: undefined,
|
||||||
|
|
||||||
|
// 通用权益信息(ad.generalRightsInfo 序列化 JSON)
|
||||||
|
generalRightsInfo,
|
||||||
|
|
||||||
|
// 权益扩展信息
|
||||||
|
rightsExtJson: query.rightsExtJson || hint.rightsExtJson || undefined,
|
||||||
|
|
||||||
|
// 应用信息(下载类广告)
|
||||||
|
appInfo: query.appInfo ? JSON.parse(query.appInfo) : undefined,
|
||||||
|
|
||||||
|
// 广告上下文(ad.adLogId.contextInfo,客户端真实来源)
|
||||||
|
contextInfo,
|
||||||
|
|
||||||
|
// 应用是否已安装(下载类广告)
|
||||||
|
installed: query.installed ? parseInt(query.installed) : undefined,
|
||||||
|
|
||||||
|
// 嗅探时间:逆向确认(classes5.dex AdDSLUtils.requestRightGain):
|
||||||
|
// rightsGainMethod==3(曝光+下载) 或 6(LAXIN) 时传 currentTimeMillis
|
||||||
|
sniffTime:
|
||||||
|
rightsGainMethod === 3 || rightsGainMethod === 6
|
||||||
|
? query.sniffTime
|
||||||
|
? parseInt(query.sniffTime)
|
||||||
|
: time
|
||||||
|
: undefined,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理 undefined 字段
|
||||||
|
Object.keys(rightsParam).forEach((key) => {
|
||||||
|
if (rightsParam[key] === undefined) delete rightsParam[key]
|
||||||
|
})
|
||||||
|
|
||||||
|
// 将参数序列化为 reqParam 字符串 (与原生源码一致)
|
||||||
|
const data = {
|
||||||
|
reqParam: JSON.stringify(rightsParam),
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await request(
|
||||||
|
`/api/ad/listening/rights/gain`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'xeapi', 'v3'),
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
code: 200,
|
||||||
|
data: res.body,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
// 私人 DJ
|
||||||
|
|
||||||
|
// 实际请求参数如下, 部分内容省略, 敏感信息已进行混淆
|
||||||
|
// 可按需修改此 API 的代码
|
||||||
|
/* {"extInfo":"{\"lastRequestTimestamp\":1692358373509,\"lbsInfoList\":[{\"lat\":40.23076381,\"lon\":129.07545186,\"time\":1692358543},{\"lat\":40.23076381,\"lon\":129.07545186,\"time\":1692055283}],\"listenedTs\":false,\"noAidjToAidj\":true}","header":"{}"} */
|
||||||
|
|
||||||
|
const logger = require('../util/logger.js')
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
var extInfo = {}
|
||||||
|
if (query.latitude != undefined) {
|
||||||
|
extInfo.lbsInfoList = [
|
||||||
|
{
|
||||||
|
lat: query.latitude,
|
||||||
|
lon: query.longitude,
|
||||||
|
time: Date.parse(new Date()) / 1000,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
extInfo.noAidjToAidj = false
|
||||||
|
extInfo.lastRequestTimestamp = new Date().getTime()
|
||||||
|
extInfo.listenedTs = false
|
||||||
|
const data = {
|
||||||
|
extInfo: JSON.stringify(extInfo),
|
||||||
|
}
|
||||||
|
// logger.info(data)
|
||||||
|
return request(`/api/aidj/content/rcmd/info`, data, createOption(query))
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
// 专辑内容
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
return request(`/api/v1/album/${query.id}`, {}, createOption(query, 'weapi'))
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
// 数字专辑详情
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
id: query.id,
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
`/api/vipmall/albumproduct/detail`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'weapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
// 专辑动态信息
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
id: query.id,
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
`/api/album/detail/dynamic`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'weapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
// 数字专辑-新碟上架
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
limit: query.limit || 30,
|
||||||
|
offset: query.offset || 0,
|
||||||
|
total: true,
|
||||||
|
area: query.area || 'ALL', //ALL:全部,ZH:华语,EA:欧美,KR:韩国,JP:日本
|
||||||
|
type: query.type,
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
`/api/vipmall/albumproduct/list`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'weapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
// 数字专辑-语种风格馆
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
limit: query.limit || 10,
|
||||||
|
offset: query.offset || 0,
|
||||||
|
total: true,
|
||||||
|
area: query.area || 'Z_H', //Z_H:华语,E_A:欧美,KR:韩国,JP:日本
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
`/api/vipmall/appalbum/album/style`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'weapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// 全部新碟
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
limit: query.limit || 30,
|
||||||
|
offset: query.offset || 0,
|
||||||
|
total: true,
|
||||||
|
area: query.area || 'ALL', //ALL:全部,ZH:华语,EA:欧美,KR:韩国,JP:日本
|
||||||
|
}
|
||||||
|
return request(`/api/album/new`, data, createOption(query, 'weapi'))
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
// 最新专辑
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
return request(`/api/discovery/newAlbum`, {}, createOption(query, 'weapi'))
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
// 获取专辑歌曲的音质
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
id: query.id,
|
||||||
|
}
|
||||||
|
return request(`/api/album/privilege`, data, createOption(query))
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
// 数字专辑&数字单曲-榜单
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
let data = {
|
||||||
|
albumType: query.albumType || 0, //0为数字专辑,1为数字单曲
|
||||||
|
}
|
||||||
|
const type = query.type || 'daily' // daily,week,year,total
|
||||||
|
if (type === 'year') {
|
||||||
|
data = {
|
||||||
|
...data,
|
||||||
|
year: query.year,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
`/api/feealbum/songsaleboard/${type}/type`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'weapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
// 收藏/取消收藏专辑
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
query.t = query.t == 1 ? 'sub' : 'unsub'
|
||||||
|
const data = {
|
||||||
|
id: query.id,
|
||||||
|
}
|
||||||
|
return request(`/api/album/${query.t}`, data, createOption(query, 'weapi'))
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// 已收藏专辑列表
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
limit: query.limit || 25,
|
||||||
|
offset: query.offset || 0,
|
||||||
|
total: true,
|
||||||
|
}
|
||||||
|
return request(`/api/album/sublist`, data, createOption(query, 'weapi'))
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
const { cookieToJson } = require('../util/index')
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const uri = query.uri
|
||||||
|
let data = {}
|
||||||
|
try {
|
||||||
|
data =
|
||||||
|
typeof query.data === 'string' ? JSON.parse(query.data) : query.data || {}
|
||||||
|
if (typeof data.cookie === 'string') {
|
||||||
|
data.cookie = cookieToJson(data.cookie)
|
||||||
|
query.cookie = data.cookie
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
data = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const crypto = query.crypto || ''
|
||||||
|
|
||||||
|
const res = request(uri, data, createOption(query, crypto))
|
||||||
|
return res
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
// 歌手专辑列表
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
limit: query.limit || 30,
|
||||||
|
offset: query.offset || 0,
|
||||||
|
total: true,
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
`/api/artist/albums/${query.id}`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'weapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
// 歌手介绍
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
id: query.id,
|
||||||
|
}
|
||||||
|
return request(`/api/artist/introduction`, data, createOption(query, 'weapi'))
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
return request(
|
||||||
|
`/api/artist/head/info/get`,
|
||||||
|
{
|
||||||
|
id: query.id,
|
||||||
|
},
|
||||||
|
createOption(query),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
// 歌手动态信息
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
id: query.id,
|
||||||
|
}
|
||||||
|
return request(`/api/artist/detail/dynamic`, data, createOption(query))
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// 歌手粉丝
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
id: query.id,
|
||||||
|
limit: query.limit || 20,
|
||||||
|
offset: query.offset || 0,
|
||||||
|
}
|
||||||
|
return request(`/api/artist/fans/get`, data, createOption(query, 'weapi'))
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
// 歌手粉丝数量
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
id: query.id,
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
`/api/artist/follow/count/get`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'weapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
// 歌手分类
|
||||||
|
|
||||||
|
/*
|
||||||
|
type 取值
|
||||||
|
1:男歌手
|
||||||
|
2:女歌手
|
||||||
|
3:乐队
|
||||||
|
|
||||||
|
area 取值
|
||||||
|
-1:全部
|
||||||
|
7华语
|
||||||
|
96欧美
|
||||||
|
8:日本
|
||||||
|
16韩国
|
||||||
|
0:其他
|
||||||
|
|
||||||
|
initial 取值 a-z/A-Z
|
||||||
|
*/
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
initial: isNaN(query.initial)
|
||||||
|
? (query.initial || '').toUpperCase().charCodeAt() || undefined
|
||||||
|
: query.initial,
|
||||||
|
offset: query.offset || 0,
|
||||||
|
limit: query.limit || 30,
|
||||||
|
total: true,
|
||||||
|
type: query.type || '1',
|
||||||
|
area: query.area,
|
||||||
|
}
|
||||||
|
return request(`/api/v1/artist/list`, data, createOption(query, 'weapi'))
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
// 歌手相关MV
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
artistId: query.id,
|
||||||
|
limit: query.limit,
|
||||||
|
offset: query.offset,
|
||||||
|
total: true,
|
||||||
|
}
|
||||||
|
return request(`/api/artist/mvs`, data, createOption(query, 'weapi'))
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
limit: query.limit || 20,
|
||||||
|
startTimestamp: query.before || Date.now(),
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
`/api/sub/artist/new/works/mv/list`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'weapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
limit: query.limit || 20,
|
||||||
|
startTimestamp: query.before || Date.now(),
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
`/api/sub/artist/new/works/song/list`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'weapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
// 获取关注歌手的新歌曲和 MV
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
startTimestamp: query.startTimestamp || query.before || Date.now(),
|
||||||
|
sourceType: query.sourceType || 1,
|
||||||
|
limit: query.limit || 10,
|
||||||
|
firstRequest: query.firstRequest ?? true,
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
`/api/sub/artist/new/works/song-mv/list/v2`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'eapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
// 获取所有关注歌手最近的 50 首新歌
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
return request(
|
||||||
|
`/api/sub/artist/new/works/song/playall`,
|
||||||
|
{},
|
||||||
|
createOption(query, 'eapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
id: query.id,
|
||||||
|
private_cloud: 'true',
|
||||||
|
work_type: 1,
|
||||||
|
order: query.order || 'hot', //hot,time
|
||||||
|
offset: query.offset || 0,
|
||||||
|
limit: query.limit || 100,
|
||||||
|
}
|
||||||
|
return request(`/api/v1/artist/songs`, data, createOption(query))
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// 收藏与取消收藏歌手
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
query.t = query.t == 1 ? 'sub' : 'unsub'
|
||||||
|
const data = {
|
||||||
|
artistId: query.id,
|
||||||
|
artistIds: '[' + query.id + ']',
|
||||||
|
}
|
||||||
|
return request(`/api/artist/${query.t}`, data, createOption(query, 'weapi'))
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// 关注歌手列表
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
limit: query.limit || 25,
|
||||||
|
offset: query.offset || 0,
|
||||||
|
total: true,
|
||||||
|
}
|
||||||
|
return request(`/api/artist/sublist`, data, createOption(query, 'weapi'))
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
// 歌手热门 50 首歌曲
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
id: query.id,
|
||||||
|
}
|
||||||
|
return request(`/api/artist/top/song`, data, createOption(query, 'weapi'))
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
// 歌手相关视频
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
artistId: query.id,
|
||||||
|
page: JSON.stringify({
|
||||||
|
size: query.size || 10,
|
||||||
|
cursor: query.cursor || 0,
|
||||||
|
}),
|
||||||
|
tab: 0,
|
||||||
|
order: query.order || 0,
|
||||||
|
}
|
||||||
|
return request(`/api/mlog/artist/video`, data, createOption(query, 'weapi'))
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
// 歌手单曲
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
return request(`/api/v1/artist/${query.id}`, {}, createOption(query, 'weapi'))
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
const { default: axios } = require('axios')
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = async (query, request) => {
|
||||||
|
const res = await axios({
|
||||||
|
method: 'get',
|
||||||
|
url: `https://interface.music.163.com/api/music/audio/match?sessionId=0123456789abcdef&algorithmCode=shazam_v2&duration=${
|
||||||
|
query.duration
|
||||||
|
}&rawdata=${encodeURIComponent(query.audioFP)}×=1&decrypt=1`,
|
||||||
|
data: null,
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
code: 200,
|
||||||
|
data: res.data.data,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
const uploadPlugin = require('../plugins/upload')
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = async (query, request) => {
|
||||||
|
const uploadInfo = await uploadPlugin(query, request)
|
||||||
|
const res = await request(
|
||||||
|
`/api/user/avatar/upload/v1`,
|
||||||
|
{
|
||||||
|
imgid: uploadInfo.imgId,
|
||||||
|
},
|
||||||
|
createOption(query),
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
code: 200,
|
||||||
|
data: {
|
||||||
|
...uploadInfo,
|
||||||
|
...res.body,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
// 首页轮播图
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const type =
|
||||||
|
{
|
||||||
|
0: 'pc',
|
||||||
|
1: 'android',
|
||||||
|
2: 'iphone',
|
||||||
|
3: 'ipad',
|
||||||
|
}[query.type || 0] || 'pc'
|
||||||
|
return request(
|
||||||
|
`/api/v2/banner/get`,
|
||||||
|
{ clientType: type },
|
||||||
|
createOption(query),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
// 批量请求接口
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {}
|
||||||
|
Object.keys(query).forEach((i) => {
|
||||||
|
if (/^\/api\//.test(i)) {
|
||||||
|
data[i] = query[i]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return request(`/api/batch`, data, createOption(query))
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// 广播电台 - 分类/地区信息
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {}
|
||||||
|
return request(
|
||||||
|
`/api/voice/broadcast/category/region/get`,
|
||||||
|
data,
|
||||||
|
createOption(query),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
// 广播电台 - 我的收藏
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
contentType: 'BROADCAST',
|
||||||
|
limit: query.limit || '99999',
|
||||||
|
timeReverseOrder: 'true',
|
||||||
|
startDate: '4762584922000',
|
||||||
|
}
|
||||||
|
return request(`/api/content/channel/collect/list`, data, createOption(query))
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
// 广播电台 - 电台信息
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
channelId: query.id,
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
`/api/voice/broadcast/channel/currentinfo`,
|
||||||
|
data,
|
||||||
|
createOption(query),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
// 广播电台 - 全部电台
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
categoryId: query.categoryId || '0',
|
||||||
|
regionId: query.regionId || '0',
|
||||||
|
limit: query.limit || '20',
|
||||||
|
lastId: query.lastId || '0',
|
||||||
|
score: query.score || '-1',
|
||||||
|
}
|
||||||
|
return request(`/api/voice/broadcast/channel/list`, data, createOption(query))
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
// 广播电台 - 收藏/取消收藏电台
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
query.t = query.t == 1 ? 'false' : 'true'
|
||||||
|
const data = {
|
||||||
|
contentType: 'BROADCAST',
|
||||||
|
contentId: query.id,
|
||||||
|
cancelCollect: query.t,
|
||||||
|
}
|
||||||
|
return request(`/api/content/interact/collect`, data, createOption(query))
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
startTime: query.startTime || Date.now(),
|
||||||
|
endTime: query.endTime || Date.now(),
|
||||||
|
}
|
||||||
|
return request(`/api/mcalendar/detail`, data, createOption(query, 'weapi'))
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
// 发送安全验证码
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
ctcode: query.ctcode || '86',
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
`/api/sms/captcha/safe/sent`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'eapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// 发送验证码
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
ctcode: query.ctcode || '86',
|
||||||
|
secrete: 'music_middleuser_pclogin',
|
||||||
|
cellphone: query.phone,
|
||||||
|
}
|
||||||
|
return request(`/api/sms/captcha/sent`, data, createOption(query, 'weapi'))
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
// 发送验证码
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
ctcode: query.ctcode || '86',
|
||||||
|
secrete: 'music_middleuser_pclogin',
|
||||||
|
cellphone: query.phone,
|
||||||
|
scene: '0',
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
`/api/middle/captcha/sent/v1`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'eapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// 校验验证码
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
ctcode: query.ctcode || '86',
|
||||||
|
cellphone: query.phone,
|
||||||
|
captcha: query.captcha,
|
||||||
|
}
|
||||||
|
return request(`/api/sms/captcha/verify`, data, createOption(query, 'weapi'))
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
// 检测手机号码是否已注册
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
cellphone: query.phone,
|
||||||
|
countrycode: query.countrycode,
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
`/api/cellphone/existence/check`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'eapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// 获取指定维度音乐排行榜详情
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
chartCode: query.chartCode,
|
||||||
|
targetId: query.targetId,
|
||||||
|
targetType: query.targetType,
|
||||||
|
}
|
||||||
|
return request(`/api/chart/detail`, data, createOption(query))
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// 获取指定维度音乐排行榜列表
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
chartCode: query.chartCode,
|
||||||
|
targetId: query.targetId,
|
||||||
|
targetType: query.targetType,
|
||||||
|
}
|
||||||
|
return request(`/api/chart/song/detail`, data, createOption(query))
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
// 歌曲可用性
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
ids: '[' + parseInt(query.id) + ']',
|
||||||
|
br: parseInt(query.br || 999000),
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
`/api/song/enhance/player/url`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'weapi'),
|
||||||
|
).then((response) => {
|
||||||
|
let playable = false
|
||||||
|
if (response.body.code == 200) {
|
||||||
|
if (response.body.data[0].code == 200) {
|
||||||
|
playable = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (playable) {
|
||||||
|
response.body = { code: 200, success: true, message: 'ok' }
|
||||||
|
return response
|
||||||
|
} else {
|
||||||
|
// response.status = 404
|
||||||
|
response.body = { code: 200, success: false, message: '亲爱的,暂无版权' }
|
||||||
|
return response
|
||||||
|
// return Promise.reject(response)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
+164
@@ -0,0 +1,164 @@
|
|||||||
|
const uploadPlugin = require('../plugins/songUpload')
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
const logger = require('../util/logger.js')
|
||||||
|
const {
|
||||||
|
isTempFile,
|
||||||
|
getFileSize,
|
||||||
|
getFileMd5,
|
||||||
|
cleanupTempFile,
|
||||||
|
getFileExtension,
|
||||||
|
sanitizeFilename,
|
||||||
|
} = require('../util/fileHelper')
|
||||||
|
|
||||||
|
let mm
|
||||||
|
module.exports = async (query, request) => {
|
||||||
|
mm = require('music-metadata')
|
||||||
|
|
||||||
|
query.songFile.name = Buffer.from(query.songFile.name, 'latin1').toString(
|
||||||
|
'utf-8',
|
||||||
|
)
|
||||||
|
const ext = getFileExtension(query.songFile.name)
|
||||||
|
const filename = sanitizeFilename(query.songFile.name)
|
||||||
|
const bitrate = 999000
|
||||||
|
|
||||||
|
if (!query.songFile) {
|
||||||
|
return Promise.reject({
|
||||||
|
status: 500,
|
||||||
|
body: {
|
||||||
|
msg: '请上传音乐文件',
|
||||||
|
code: 500,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const useTemp = isTempFile(query.songFile)
|
||||||
|
let fileSize = await getFileSize(query.songFile)
|
||||||
|
let fileMd5 = await getFileMd5(query.songFile)
|
||||||
|
|
||||||
|
query.songFile.md5 = fileMd5
|
||||||
|
query.songFile.size = fileSize
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await request(
|
||||||
|
`/api/cloud/upload/check`,
|
||||||
|
{
|
||||||
|
bitrate: String(bitrate),
|
||||||
|
ext: '',
|
||||||
|
length: fileSize,
|
||||||
|
md5: fileMd5,
|
||||||
|
songId: '0',
|
||||||
|
version: 1,
|
||||||
|
},
|
||||||
|
createOption(query),
|
||||||
|
)
|
||||||
|
|
||||||
|
let artist = ''
|
||||||
|
let album = ''
|
||||||
|
let songName = ''
|
||||||
|
|
||||||
|
try {
|
||||||
|
let metadata
|
||||||
|
if (useTemp) {
|
||||||
|
metadata = await mm.parseFile(query.songFile.tempFilePath)
|
||||||
|
} else {
|
||||||
|
metadata = await mm.parseBuffer(
|
||||||
|
query.songFile.data,
|
||||||
|
query.songFile.mimetype,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const info = metadata.common
|
||||||
|
if (info.title) songName = info.title
|
||||||
|
if (info.album) album = info.album
|
||||||
|
if (info.artist) artist = info.artist
|
||||||
|
} catch (error) {
|
||||||
|
logger.info('元数据解析错误:', error.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokenRes = await request(
|
||||||
|
`/api/nos/token/alloc`,
|
||||||
|
{
|
||||||
|
bucket: '',
|
||||||
|
ext: ext,
|
||||||
|
filename: filename,
|
||||||
|
local: false,
|
||||||
|
nos_product: 3,
|
||||||
|
type: 'audio',
|
||||||
|
md5: fileMd5,
|
||||||
|
},
|
||||||
|
createOption(query),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!tokenRes.body.result || !tokenRes.body.result.resourceId) {
|
||||||
|
logger.error('Token分配失败:', tokenRes.body)
|
||||||
|
return Promise.reject({
|
||||||
|
status: 500,
|
||||||
|
body: {
|
||||||
|
code: 500,
|
||||||
|
msg: '获取上传token失败',
|
||||||
|
detail: tokenRes.body,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (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,39 @@
|
|||||||
|
// 云盘导入歌曲
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = async (query, request) => {
|
||||||
|
query.id = query.id || -2
|
||||||
|
query.artist = query.artist || '未知'
|
||||||
|
query.album = query.album || '未知'
|
||||||
|
const checkData = {
|
||||||
|
uploadType: 0,
|
||||||
|
songs: JSON.stringify([
|
||||||
|
{
|
||||||
|
md5: query.md5,
|
||||||
|
songId: query.id,
|
||||||
|
bitrate: query.bitrate,
|
||||||
|
fileSize: query.fileSize,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
}
|
||||||
|
const res = await request(
|
||||||
|
`/api/cloud/upload/check/v2`,
|
||||||
|
checkData,
|
||||||
|
createOption(query),
|
||||||
|
)
|
||||||
|
//res.body.data[0].upload 0:文件可导入,1:文件已在云盘,2:不能导入
|
||||||
|
//只能用song决定云盘文件名,且上传后的文件名后缀固定为mp3
|
||||||
|
const importData = {
|
||||||
|
uploadType: 0,
|
||||||
|
songs: JSON.stringify([
|
||||||
|
{
|
||||||
|
songId: res.body.data[0].songId,
|
||||||
|
bitrate: query.bitrate,
|
||||||
|
song: query.song,
|
||||||
|
artist: query.artist,
|
||||||
|
album: query.album,
|
||||||
|
fileName: query.song + '.' + query.fileType,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
}
|
||||||
|
return request(`/api/cloud/user/song/import`, importData, createOption(query))
|
||||||
|
}
|
||||||
@@ -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,13 @@
|
|||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
userId: query.uid,
|
||||||
|
songId: query.sid,
|
||||||
|
adjustSongId: query.asid,
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
`/api/cloud/user/song/match`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'weapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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,13 @@
|
|||||||
|
// 搜索
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
s: query.keywords,
|
||||||
|
type: query.type || 1, // 1: 单曲, 10: 专辑, 100: 歌手, 1000: 歌单, 1002: 用户, 1004: MV, 1006: 歌词, 1009: 电台, 1014: 视频
|
||||||
|
limit: query.limit || 30,
|
||||||
|
offset: query.offset || 0,
|
||||||
|
total: true,
|
||||||
|
}
|
||||||
|
return request(`/api/cloudsearch/pc`, data, createOption(query))
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
const { resourceTypeMap } = require('../util/config.json')
|
||||||
|
// 发送与删除评论
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
query.t = {
|
||||||
|
1: 'add',
|
||||||
|
0: 'delete',
|
||||||
|
2: 'reply',
|
||||||
|
}[query.t]
|
||||||
|
query.type = resourceTypeMap[query.type]
|
||||||
|
const data = {
|
||||||
|
threadId: query.type + query.id,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.type == 'A_EV_2_') {
|
||||||
|
data.threadId = query.threadId
|
||||||
|
}
|
||||||
|
if (query.t == 'add') data.content = query.content
|
||||||
|
else if (query.t == 'delete') data.commentId = query.commentId
|
||||||
|
else if (query.t == 'reply') {
|
||||||
|
data.commentId = query.commentId
|
||||||
|
data.content = query.content
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
`/api/resource/comments/${query.t}`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'eapi', 'v2'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
const { resourceTypeMap } = require('../util/config.json')
|
||||||
|
// 发送评论
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
threadId: resourceTypeMap[query.type] + query.id,
|
||||||
|
content: query.content,
|
||||||
|
resourceType: '0',
|
||||||
|
expressionPicId: '-1',
|
||||||
|
bubbleId: '-1',
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
`/api/resource/comments/add`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'xeapi', 'v3'),
|
||||||
|
)
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user