qqmusic-api-python 0.3.4__tar.gz → 0.3.6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.6}/.gitignore +2 -1
- {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.6}/PKG-INFO +4 -4
- {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.6}/README.md +1 -1
- {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.6}/pyproject.toml +6 -7
- {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.6}/qqmusic_api/__init__.py +3 -2
- qqmusic_api_python-0.3.6/qqmusic_api/comment.py +183 -0
- {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.6}/qqmusic_api/login.py +10 -4
- qqmusic_api_python-0.3.6/qqmusic_api/recommend.py +52 -0
- {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.6}/qqmusic_api/song.py +1 -0
- {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.6}/qqmusic_api/utils/network.py +8 -9
- qqmusic_api_python-0.3.6/qqmusic_api/utils/sign.py +36 -0
- qqmusic_api_python-0.3.6/tests/test_comment.py +19 -0
- qqmusic_api_python-0.3.6/tests/test_recommend.py +25 -0
- {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.6}/tests/test_song.py +1 -1
- qqmusic_api_python-0.3.4/qqmusic_api/utils/sign.py +0 -68
- {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.6}/LICENSE +0 -0
- {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.6}/qqmusic_api/album.py +0 -0
- {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.6}/qqmusic_api/exceptions/__init__.py +0 -0
- {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.6}/qqmusic_api/exceptions/api_exception.py +0 -0
- {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.6}/qqmusic_api/lyric.py +0 -0
- {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.6}/qqmusic_api/mv.py +0 -0
- {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.6}/qqmusic_api/search.py +0 -0
- {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.6}/qqmusic_api/singer.py +0 -0
- {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.6}/qqmusic_api/songlist.py +0 -0
- {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.6}/qqmusic_api/top.py +0 -0
- {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.6}/qqmusic_api/user.py +0 -0
- {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.6}/qqmusic_api/utils/__init__.py +0 -0
- {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.6}/qqmusic_api/utils/common.py +0 -0
- {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.6}/qqmusic_api/utils/credential.py +0 -0
- {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.6}/qqmusic_api/utils/device.py +0 -0
- {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.6}/qqmusic_api/utils/qimei.py +0 -0
- {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.6}/qqmusic_api/utils/session.py +0 -0
- {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.6}/qqmusic_api/utils/tripledes.py +0 -0
- {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.6}/tests/test_album.py +0 -0
- {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.6}/tests/test_login.py +0 -0
- {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.6}/tests/test_lyric.py +0 -0
- {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.6}/tests/test_mv.py +0 -0
- {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.6}/tests/test_qimei.py +0 -0
- {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.6}/tests/test_search.py +0 -0
- {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.6}/tests/test_session.py +0 -0
- {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.6}/tests/test_sign.py +0 -0
- {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.6}/tests/test_singer.py +0 -0
- {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.6}/tests/test_songlist.py +0 -0
- {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.6}/tests/test_top.py +0 -0
- {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.6}/tests/test_user.py +0 -0
- {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.6}/web/README.md +0 -0
|
@@ -30,6 +30,7 @@ share/python-wheels/
|
|
|
30
30
|
.installed.cfg
|
|
31
31
|
*.egg
|
|
32
32
|
MANIFEST
|
|
33
|
+
tools
|
|
33
34
|
|
|
34
35
|
# PyInstaller
|
|
35
36
|
# Usually these files are written by a python script from a template
|
|
@@ -164,7 +165,7 @@ cython_debug/
|
|
|
164
165
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
165
166
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
166
167
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
167
|
-
|
|
168
|
+
.idea/
|
|
168
169
|
|
|
169
170
|
### Python Patch ###
|
|
170
171
|
# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: qqmusic-api-python
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.6
|
|
4
4
|
Summary: QQ音乐API封装库
|
|
5
5
|
Project-URL: homepage, https://luren-dc.github.io/QQMusicApi/
|
|
6
6
|
Project-URL: repository, https://github.com/luren-dc/QQMusicApi
|
|
@@ -17,12 +17,12 @@ Classifier: License :: OSI Approved :: MIT License
|
|
|
17
17
|
Classifier: Natural Language :: Chinese (Simplified)
|
|
18
18
|
Classifier: Programming Language :: Python
|
|
19
19
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
20
|
-
Classifier: Programming Language :: Python :: 3.
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
21
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
22
22
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
23
|
Requires-Python: >=3.10
|
|
24
24
|
Requires-Dist: aiocache>=0.12.3
|
|
25
|
-
Requires-Dist: cryptography<
|
|
25
|
+
Requires-Dist: cryptography<45.0.6,>=45.0.5
|
|
26
26
|
Requires-Dist: httpx[http2]>=0.27.0
|
|
27
27
|
Requires-Dist: orjson>=3.10.15
|
|
28
28
|
Requires-Dist: typing-extensions>=4.12.2
|
|
@@ -100,7 +100,7 @@ asyncio.run(main())
|
|
|
100
100
|
|
|
101
101
|
## [Web API](./web/README.md)
|
|
102
102
|
|
|
103
|
-
##
|
|
103
|
+
## License
|
|
104
104
|
|
|
105
105
|
本项目基于 **[MIT License](https://github.com/luren-dc/QQMusicApi?tab=MIT-1-ov-file)** 许可证发行。
|
|
106
106
|
|
|
@@ -5,7 +5,7 @@ authors = [
|
|
|
5
5
|
{ name = "Luren", email = "68656403+luren-dc@users.noreply.github.com" },
|
|
6
6
|
]
|
|
7
7
|
dependencies = [
|
|
8
|
-
"cryptography>=
|
|
8
|
+
"cryptography>=45.0.5,<45.0.6",
|
|
9
9
|
"typing-extensions>=4.12.2",
|
|
10
10
|
"httpx[http2]>=0.27.0",
|
|
11
11
|
"aiocache>=0.12.3",
|
|
@@ -26,7 +26,7 @@ classifiers = [
|
|
|
26
26
|
"Framework :: aiohttp",
|
|
27
27
|
"Programming Language :: Python",
|
|
28
28
|
"Programming Language :: Python :: 3 :: Only",
|
|
29
|
-
"Programming Language :: Python :: 3.
|
|
29
|
+
"Programming Language :: Python :: 3.10",
|
|
30
30
|
"Programming Language :: Python :: Implementation :: CPython",
|
|
31
31
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
32
32
|
]
|
|
@@ -55,12 +55,12 @@ path = "qqmusic_api/__init__.py"
|
|
|
55
55
|
packages = ["qqmusic_api"]
|
|
56
56
|
|
|
57
57
|
[tool.hatch.build.targets.sdist]
|
|
58
|
-
include = ["/qqmusic_api", "/tests", "
|
|
58
|
+
include = ["/qqmusic_api", "/tests", "LICENSE", "README.md"]
|
|
59
59
|
|
|
60
60
|
[dependency-groups]
|
|
61
61
|
testing = [
|
|
62
62
|
"pytest<9.0.0,>=8.2.0",
|
|
63
|
-
"pytest-asyncio<1.
|
|
63
|
+
"pytest-asyncio<1.1.0,>=1.0.0",
|
|
64
64
|
"pytest-sugar<2.0.0,>=1.0.0",
|
|
65
65
|
]
|
|
66
66
|
docs = [
|
|
@@ -81,7 +81,7 @@ web = [
|
|
|
81
81
|
[tool.commitizen]
|
|
82
82
|
name = "cz_gitmoji"
|
|
83
83
|
|
|
84
|
-
[tool.
|
|
84
|
+
[tool.pyright]
|
|
85
85
|
venvPath = "."
|
|
86
86
|
venv = ".venv"
|
|
87
87
|
include = ["qqmusic_api"]
|
|
@@ -104,10 +104,9 @@ extend-select = [
|
|
|
104
104
|
"ASYNC",
|
|
105
105
|
"C4",
|
|
106
106
|
"FURB",
|
|
107
|
-
"R",
|
|
108
107
|
"PERF",
|
|
109
108
|
]
|
|
110
|
-
ignore = ["D105", "D107", "D205", "D415"]
|
|
109
|
+
ignore = ["D105", "D107", "D205", "D415", "RUF029"]
|
|
111
110
|
pydocstyle = { convention = "google" }
|
|
112
111
|
|
|
113
112
|
[tool.ruff.lint.per-file-ignores]
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
|
|
3
|
-
from . import album, login, lyric, mv, search, singer, song, songlist, top, user
|
|
3
|
+
from . import album, comment, login, lyric, mv, search, singer, song, songlist, top, user
|
|
4
4
|
from .utils.credential import Credential
|
|
5
5
|
from .utils.session import Session, get_session, set_session
|
|
6
6
|
|
|
7
|
-
__version__ = "0.3.
|
|
7
|
+
__version__ = "0.3.6"
|
|
8
8
|
|
|
9
9
|
logger = logging.getLogger("qqmusicapi")
|
|
10
10
|
|
|
@@ -13,6 +13,7 @@ __all__ = [
|
|
|
13
13
|
"Credential",
|
|
14
14
|
"Session",
|
|
15
15
|
"album",
|
|
16
|
+
"comment",
|
|
16
17
|
"get_session",
|
|
17
18
|
"login",
|
|
18
19
|
"lyric",
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""评论 API"""
|
|
2
|
+
|
|
3
|
+
from typing import Any, cast
|
|
4
|
+
|
|
5
|
+
from .utils.network import api_request
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@api_request("music.globalComment.CommentCountSrv", "GetCmCount")
|
|
9
|
+
async def get_comment_count(biz_id: str):
|
|
10
|
+
"""获取歌曲评论数量
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
biz_id: 歌曲 ID
|
|
14
|
+
"""
|
|
15
|
+
return {
|
|
16
|
+
"request": {
|
|
17
|
+
"biz_id": biz_id,
|
|
18
|
+
"biz_type": 1,
|
|
19
|
+
"biz_sub_type": 2,
|
|
20
|
+
},
|
|
21
|
+
}, lambda data: cast(dict[str, Any], data.get("response", {}))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _processor(data: dict[str, Any]):
|
|
25
|
+
"""处理并返回结构化评论数据:
|
|
26
|
+
|
|
27
|
+
返回结构:
|
|
28
|
+
[
|
|
29
|
+
{
|
|
30
|
+
"Avatar": str, # 用户头像 URL
|
|
31
|
+
"CmId": str, # 评论 ID (后续需要获取全部子评论时需用到)
|
|
32
|
+
"PraiseNum": int, # 点赞数
|
|
33
|
+
"Nick": str, # 昵称
|
|
34
|
+
"Pic": str, # 评论配图 (可能为空)
|
|
35
|
+
"Content": str, # 评论内容
|
|
36
|
+
"SeqNo": str, # 评论序号 ID 可以用于传递给 参数: last_comment_seq_no
|
|
37
|
+
"SubComments": [ # 子评论列表
|
|
38
|
+
{
|
|
39
|
+
"Avatar": str,
|
|
40
|
+
"Nick": str,
|
|
41
|
+
"Content": str,
|
|
42
|
+
"Pic": str,
|
|
43
|
+
"PraiseNum": int,
|
|
44
|
+
"SeqNo": str
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
},
|
|
48
|
+
...
|
|
49
|
+
]
|
|
50
|
+
"""
|
|
51
|
+
comments = data.get("CommentList", {}).get("Comments", [])
|
|
52
|
+
result = []
|
|
53
|
+
|
|
54
|
+
for comment in comments:
|
|
55
|
+
item = {
|
|
56
|
+
"Avatar": comment.get("Avatar"),
|
|
57
|
+
"CmId": comment.get("CmId"),
|
|
58
|
+
"PraiseNum": comment.get("PraiseNum"),
|
|
59
|
+
"Nick": comment.get("Nick"),
|
|
60
|
+
"Pic": comment.get("Pic"),
|
|
61
|
+
"Content": comment.get("Content"),
|
|
62
|
+
"SeqNo": comment.get("SeqNo"),
|
|
63
|
+
"SubComments": [],
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
for sub in comment.get("SubComments", []):
|
|
67
|
+
sub_item = {
|
|
68
|
+
"Avatar": sub.get("Avatar"),
|
|
69
|
+
"Nick": sub.get("Nick"),
|
|
70
|
+
"Content": sub.get("Content"),
|
|
71
|
+
"Pic": sub.get("Pic"),
|
|
72
|
+
"PraiseNum": sub.get("PraiseNum"),
|
|
73
|
+
"SeqNo": sub.get("SeqNo"),
|
|
74
|
+
}
|
|
75
|
+
item["SubComments"].append(sub_item)
|
|
76
|
+
|
|
77
|
+
result.append(item)
|
|
78
|
+
|
|
79
|
+
return result
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@api_request("music.globalComment.CommentRead", "GetHotCommentList")
|
|
83
|
+
async def get_hot_comments(
|
|
84
|
+
biz_id: str,
|
|
85
|
+
page_num: int = 1,
|
|
86
|
+
page_size: int = 15,
|
|
87
|
+
last_comment_seq_no: str = "",
|
|
88
|
+
):
|
|
89
|
+
"""获取歌曲热评
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
biz_id: 歌曲 ID
|
|
93
|
+
page_num: 页码
|
|
94
|
+
page_size: 每页数量
|
|
95
|
+
last_comment_seq_no: 上一页最后一条评论 ID(可选)
|
|
96
|
+
"""
|
|
97
|
+
params = {
|
|
98
|
+
"BizType": 1,
|
|
99
|
+
"BizId": biz_id,
|
|
100
|
+
"LastCommentSeqNo": last_comment_seq_no,
|
|
101
|
+
"PageSize": page_size,
|
|
102
|
+
"PageNum": page_num - 1,
|
|
103
|
+
"HotType": 1,
|
|
104
|
+
"WithAirborne": 0,
|
|
105
|
+
"PicEnable": 1,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return params, _processor
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@api_request("music.globalComment.CommentRead", "GetNewCommentList")
|
|
112
|
+
async def get_new_comments(
|
|
113
|
+
biz_id: str,
|
|
114
|
+
page_num: int = 1,
|
|
115
|
+
page_size: int = 15,
|
|
116
|
+
last_comment_seq_no: str = "",
|
|
117
|
+
):
|
|
118
|
+
"""获取歌曲最新评论
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
biz_id: 歌曲 ID
|
|
122
|
+
page_num: 页码
|
|
123
|
+
page_size: 每页数量
|
|
124
|
+
last_comment_seq_no: 上一页最后一条评论 ID(可选)
|
|
125
|
+
"""
|
|
126
|
+
params = {
|
|
127
|
+
# "LastRspVer": "",
|
|
128
|
+
# "LastTotalVer": "1755832873618224522",
|
|
129
|
+
"PageSize": page_size,
|
|
130
|
+
"PageNum": page_num - 1,
|
|
131
|
+
"HashTagID": "",
|
|
132
|
+
"BizType": 1,
|
|
133
|
+
# "LastCommentId": "",
|
|
134
|
+
"PicEnable": 1,
|
|
135
|
+
"LastCommentSeqNo": last_comment_seq_no,
|
|
136
|
+
"SelfSeeEnable": 1,
|
|
137
|
+
# "LastTotal": 325,
|
|
138
|
+
# "CmListUIVer": 1,
|
|
139
|
+
"BizId": biz_id,
|
|
140
|
+
"AudioEnable": 1,
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return params, _processor
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@api_request("music.globalComment.CommentRead", "GetRecCommentList")
|
|
147
|
+
async def get_recommend_comments(
|
|
148
|
+
biz_id: str,
|
|
149
|
+
page_num: int = 1,
|
|
150
|
+
page_size: int = 15,
|
|
151
|
+
last_comment_seq_no: str = "",
|
|
152
|
+
):
|
|
153
|
+
"""获取歌曲推荐评论
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
biz_id: 歌曲 ID
|
|
157
|
+
page_num: 页码
|
|
158
|
+
page_size: 每页数量
|
|
159
|
+
last_comment_seq_no: 上一页最后一条评论 ID(可选)
|
|
160
|
+
"""
|
|
161
|
+
params = {
|
|
162
|
+
# "FromParentCmId": "",
|
|
163
|
+
# "LastRspVer": "1755834843787200911",
|
|
164
|
+
# "LastTotalVer": "1755834843679664122",
|
|
165
|
+
# "RecOffset": 0,
|
|
166
|
+
# "LastHotScore": "",
|
|
167
|
+
# "FromCommentId": "",
|
|
168
|
+
# "HashTagID": "",
|
|
169
|
+
# "CommentIds": [],
|
|
170
|
+
# "LastRecScore": "",
|
|
171
|
+
# "LastTotal": 325,
|
|
172
|
+
"PageSize": page_size,
|
|
173
|
+
"PageNum": page_num - 1,
|
|
174
|
+
"BizType": 1,
|
|
175
|
+
"PicEnable": 1,
|
|
176
|
+
"Flag": 1,
|
|
177
|
+
"LastCommentSeqNo": last_comment_seq_no,
|
|
178
|
+
"CmListUIVer": 1,
|
|
179
|
+
"BizId": biz_id,
|
|
180
|
+
"AudioEnable": 1,
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return params, _processor
|
|
@@ -12,7 +12,7 @@ from uuid import uuid4
|
|
|
12
12
|
|
|
13
13
|
import httpx
|
|
14
14
|
|
|
15
|
-
from .exceptions.api_exception import CredentialExpiredError, LoginError
|
|
15
|
+
from .exceptions.api_exception import CredentialExpiredError, LoginError, ResponseCodeError
|
|
16
16
|
from .utils.common import hash33
|
|
17
17
|
from .utils.credential import Credential
|
|
18
18
|
from .utils.network import ApiRequest
|
|
@@ -339,7 +339,7 @@ async def _authorize_qq_qr(uin: str, sigx: str) -> Credential:
|
|
|
339
339
|
data={
|
|
340
340
|
"response_type": "code",
|
|
341
341
|
"client_id": "100497308",
|
|
342
|
-
"redirect_uri": "https://y.qq.com/portal/wx_redirect.html?login_type=1&surl=https
|
|
342
|
+
"redirect_uri": "https://y.qq.com/portal/wx_redirect.html?login_type=1&surl=https://y.qq.com/",
|
|
343
343
|
"scope": "get_user_info,get_app_friends",
|
|
344
344
|
"state": "state",
|
|
345
345
|
"switch": "",
|
|
@@ -359,8 +359,8 @@ async def _authorize_qq_qr(uin: str, sigx: str) -> Credential:
|
|
|
359
359
|
raise LoginError("[QQLogin] 获取 code 失败")
|
|
360
360
|
try:
|
|
361
361
|
api = ApiRequest[[], dict[str, Any]](
|
|
362
|
-
"
|
|
363
|
-
"
|
|
362
|
+
"QQConnectLogin.LoginServer",
|
|
363
|
+
"QQLogin",
|
|
364
364
|
common={"tmeLoginType": "2"},
|
|
365
365
|
params={"code": code},
|
|
366
366
|
cacheable=False,
|
|
@@ -368,6 +368,10 @@ async def _authorize_qq_qr(uin: str, sigx: str) -> Credential:
|
|
|
368
368
|
return Credential.from_cookies_dict(await api())
|
|
369
369
|
except CredentialExpiredError:
|
|
370
370
|
raise LoginError("[QQLogin] 无法重复鉴权")
|
|
371
|
+
except ResponseCodeError as e:
|
|
372
|
+
if e.code == 20274:
|
|
373
|
+
raise LoginError("[QQLogin] 设备数量限制")
|
|
374
|
+
raise LoginError(f"[QQLogin] 未知错误: {e.code} - {e.message}")
|
|
371
375
|
|
|
372
376
|
|
|
373
377
|
async def _authorize_wx_qr(code: str) -> Credential:
|
|
@@ -431,6 +435,8 @@ async def phone_authorize(phone: int, auth_code: int, country_code: int = 86) ->
|
|
|
431
435
|
cacheable=False,
|
|
432
436
|
)()
|
|
433
437
|
match resp["code"]:
|
|
438
|
+
case 20274:
|
|
439
|
+
raise LoginError("[PhoneLogin] 设备数量限制")
|
|
434
440
|
case 20271:
|
|
435
441
|
raise LoginError("[PhoneLogin] 验证码错误或已鉴权")
|
|
436
442
|
case 0:
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""推荐相关 API"""
|
|
2
|
+
|
|
3
|
+
from .utils.network import NO_PROCESSOR, api_request
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@api_request("music.recommend.RecommendFeed", "get_recommend_feed")
|
|
7
|
+
async def get_home_feed():
|
|
8
|
+
"""获取主页推荐"""
|
|
9
|
+
return {
|
|
10
|
+
"direction": 0,
|
|
11
|
+
"page": 1,
|
|
12
|
+
"s_num": 0,
|
|
13
|
+
}, NO_PROCESSOR
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@api_request("music.radioProxy.MbTrackRadioSvr", "get_radio_track")
|
|
17
|
+
async def get_guess_recommend():
|
|
18
|
+
"""获取猜你喜欢"""
|
|
19
|
+
return {
|
|
20
|
+
"id": 99,
|
|
21
|
+
"num": 5,
|
|
22
|
+
"from": 0,
|
|
23
|
+
"scene": 0,
|
|
24
|
+
"song_ids": [],
|
|
25
|
+
"ext": {"bluetooth": ""},
|
|
26
|
+
"should_count_down": 1,
|
|
27
|
+
}, NO_PROCESSOR
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@api_request("music.recommend.TrackRelationServer", "GetRadarSong")
|
|
31
|
+
async def get_radar_recommend():
|
|
32
|
+
"""获取雷达推荐"""
|
|
33
|
+
return {
|
|
34
|
+
"Page": 1,
|
|
35
|
+
# "LastToastTime": 1755782480,
|
|
36
|
+
"ReqType": 0,
|
|
37
|
+
"FavSongs": [],
|
|
38
|
+
"EntranceSongs": [],
|
|
39
|
+
# "ext": {"bluetooth": ""},
|
|
40
|
+
}, NO_PROCESSOR
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@api_request("music.playlist.PlaylistSquare", "GetRecommendFeed")
|
|
44
|
+
async def get_recommend_songlist():
|
|
45
|
+
"""获取推荐歌单"""
|
|
46
|
+
return {"From": 0, "Size": 25}, NO_PROCESSOR
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@api_request("newsong.NewSongServer", "get_new_song_info")
|
|
50
|
+
async def get_recommend_newsong():
|
|
51
|
+
"""获取推荐新歌"""
|
|
52
|
+
return {"type": 5}, NO_PROCESSOR
|
|
@@ -38,8 +38,8 @@ def api_request(
|
|
|
38
38
|
process_bool: bool = True,
|
|
39
39
|
cache_ttl: int | None = None,
|
|
40
40
|
cacheable: bool = True,
|
|
41
|
-
exclude_params: list[str] =
|
|
42
|
-
catch_error_code: list[int] =
|
|
41
|
+
exclude_params: list[str] | None = None,
|
|
42
|
+
catch_error_code: list[int] | None = None,
|
|
43
43
|
):
|
|
44
44
|
"""API请求"""
|
|
45
45
|
|
|
@@ -111,7 +111,7 @@ class BaseRequest(ABC):
|
|
|
111
111
|
return common
|
|
112
112
|
|
|
113
113
|
@common.setter
|
|
114
|
-
def
|
|
114
|
+
def common(self, value: dict[str, Any]):
|
|
115
115
|
"""设置公共参数"""
|
|
116
116
|
self._common = value
|
|
117
117
|
|
|
@@ -194,8 +194,8 @@ class ApiRequest(BaseRequest, Generic[_P, _R]):
|
|
|
194
194
|
process_bool: bool = True,
|
|
195
195
|
cache_ttl: int | None = None,
|
|
196
196
|
cacheable: bool = True,
|
|
197
|
-
exclude_params: list[str] =
|
|
198
|
-
catch_error_code: list[int] =
|
|
197
|
+
exclude_params: list[str] | None = None,
|
|
198
|
+
catch_error_code: list[int] | None = None,
|
|
199
199
|
) -> None:
|
|
200
200
|
super().__init__(common, credential, verify, ignore_code)
|
|
201
201
|
self.module = module
|
|
@@ -206,8 +206,8 @@ class ApiRequest(BaseRequest, Generic[_P, _R]):
|
|
|
206
206
|
self.processor: Callable[[dict[str, Any]], Any] = NO_PROCESSOR
|
|
207
207
|
self.cacheable = cacheable
|
|
208
208
|
self.cache_ttl = cache_ttl
|
|
209
|
-
self.exclude_params = exclude_params
|
|
210
|
-
self.catch_error_code = catch_error_code
|
|
209
|
+
self.exclude_params = exclude_params or []
|
|
210
|
+
self.catch_error_code = catch_error_code or []
|
|
211
211
|
|
|
212
212
|
def copy(self) -> "ApiRequest[_P, _R]":
|
|
213
213
|
"""创建当前 ApiRequest 实例的副本"""
|
|
@@ -353,7 +353,7 @@ class RequestGroup(BaseRequest):
|
|
|
353
353
|
|
|
354
354
|
self._requests.append(
|
|
355
355
|
RequestItem(
|
|
356
|
-
id=len(self._requests)
|
|
356
|
+
id=len(self._requests),
|
|
357
357
|
key=unique_key,
|
|
358
358
|
request=request,
|
|
359
359
|
args=args,
|
|
@@ -422,7 +422,6 @@ class RequestGroup(BaseRequest):
|
|
|
422
422
|
|
|
423
423
|
if not self._requests:
|
|
424
424
|
return self._results
|
|
425
|
-
|
|
426
425
|
resp = await self.request()
|
|
427
426
|
await self._process_response(resp)
|
|
428
427
|
return self._results
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""QQ音乐 sign"""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from base64 import b64encode
|
|
5
|
+
from hashlib import sha1
|
|
6
|
+
|
|
7
|
+
import orjson as json
|
|
8
|
+
|
|
9
|
+
PART_1_INDEXES = [23, 14, 6, 36, 16, 40, 7, 19]
|
|
10
|
+
PART_2_INDEXES = [16, 1, 32, 12, 19, 27, 8, 5]
|
|
11
|
+
SCRAMBLE_VALUES = [89, 39, 179, 150, 218, 82, 58, 252, 177, 52, 186, 123, 120, 64, 242, 133, 143, 161, 121, 179]
|
|
12
|
+
|
|
13
|
+
# JavaScript quirks emulation
|
|
14
|
+
PART_1_INDEXES = filter(lambda x: x < 40, PART_1_INDEXES)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def sign(request: dict) -> str:
|
|
18
|
+
"""QQ音乐 请求签名
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
request: 请求数据
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
签名结果
|
|
25
|
+
"""
|
|
26
|
+
hash = sha1(json.dumps(request)).hexdigest().upper()
|
|
27
|
+
|
|
28
|
+
part1 = "".join(hash[i] for i in PART_1_INDEXES)
|
|
29
|
+
part2 = "".join(hash[i] for i in PART_2_INDEXES)
|
|
30
|
+
|
|
31
|
+
part3 = bytearray(20)
|
|
32
|
+
for i, v in enumerate(SCRAMBLE_VALUES):
|
|
33
|
+
value = v ^ int(hash[i * 2 : i * 2 + 2], 16)
|
|
34
|
+
part3[i] = value
|
|
35
|
+
b64_part = re.sub(rb"[\\/+=]", b"", b64encode(part3)).decode("utf-8")
|
|
36
|
+
return f"zzc{part1}{b64_part}{part2}".lower()
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from qqmusic_api.comment import get_comment_count, get_hot_comments
|
|
4
|
+
|
|
5
|
+
pytestmark = pytest.mark.asyncio(loop_scope="session")
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
async def test_get_comment():
|
|
9
|
+
comment = await get_hot_comments(
|
|
10
|
+
"542574330",
|
|
11
|
+
1,
|
|
12
|
+
10,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
assert comment[0]["Content"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
async def test_get_comment_count():
|
|
19
|
+
assert await get_comment_count("103540151")
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from qqmusic_api import recommend
|
|
4
|
+
|
|
5
|
+
pytestmark = pytest.mark.asyncio(loop_scope="session")
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
async def test_get_home_feed():
|
|
9
|
+
assert await recommend.get_home_feed()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
async def test_get_guess_recommend():
|
|
13
|
+
assert await recommend.get_guess_recommend()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
async def test_get_radar_recommend():
|
|
17
|
+
assert await recommend.get_radar_recommend()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
async def test_get_recommend_songlist():
|
|
21
|
+
assert await recommend.get_recommend_songlist()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
async def test_get_recommend_newsong():
|
|
25
|
+
assert await recommend.get_recommend_newsong()
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
"""QQ音乐 sign"""
|
|
2
|
-
|
|
3
|
-
import base64
|
|
4
|
-
|
|
5
|
-
import orjson as json
|
|
6
|
-
|
|
7
|
-
from .common import calc_md5
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def _head(b: bytes) -> list:
|
|
11
|
-
p = [21, 4, 9, 26, 16, 20, 27, 30]
|
|
12
|
-
return [b[x] for x in p]
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def _tail(b: bytes) -> list:
|
|
16
|
-
p = [18, 11, 3, 2, 1, 7, 6, 25]
|
|
17
|
-
return [b[x] for x in p]
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def _middle(b: bytes) -> list:
|
|
21
|
-
zd = {
|
|
22
|
-
"0": 0,
|
|
23
|
-
"1": 1,
|
|
24
|
-
"2": 2,
|
|
25
|
-
"3": 3,
|
|
26
|
-
"4": 4,
|
|
27
|
-
"5": 5,
|
|
28
|
-
"6": 6,
|
|
29
|
-
"7": 7,
|
|
30
|
-
"8": 8,
|
|
31
|
-
"9": 9,
|
|
32
|
-
"A": 10,
|
|
33
|
-
"B": 11,
|
|
34
|
-
"C": 12,
|
|
35
|
-
"D": 13,
|
|
36
|
-
"E": 14,
|
|
37
|
-
"F": 15,
|
|
38
|
-
}
|
|
39
|
-
ol = [212, 45, 80, 68, 195, 163, 163, 203, 157, 220, 254, 91, 204, 79, 104, 6]
|
|
40
|
-
res = []
|
|
41
|
-
j = 0
|
|
42
|
-
for i in range(0, len(b), 2):
|
|
43
|
-
one = zd[chr(b[i])]
|
|
44
|
-
two = zd[chr(b[i + 1])]
|
|
45
|
-
r = one * 16 ^ two
|
|
46
|
-
res.append(r ^ ol[j])
|
|
47
|
-
j += 1
|
|
48
|
-
return res
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def sign(request: dict) -> str:
|
|
52
|
-
"""QQ音乐 请求签名
|
|
53
|
-
|
|
54
|
-
Args:
|
|
55
|
-
request: 请求数据
|
|
56
|
-
|
|
57
|
-
Returns:
|
|
58
|
-
签名结果
|
|
59
|
-
"""
|
|
60
|
-
md5_str = calc_md5(json.dumps(request)).upper().encode("utf-8")
|
|
61
|
-
|
|
62
|
-
h = _head(md5_str)
|
|
63
|
-
e = _tail(md5_str)
|
|
64
|
-
ls = _middle(md5_str)
|
|
65
|
-
m = base64.b64encode(bytes(ls)).decode("utf-8")
|
|
66
|
-
|
|
67
|
-
res = "zzb" + "".join(map(chr, h)) + m + "".join(map(chr, e))
|
|
68
|
-
return res.lower().replace("/", "").replace("+", "").replace("=", "")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.6}/qqmusic_api/exceptions/api_exception.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|