qqmusic-api-python 0.2.1__tar.gz → 0.3.0__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.
Files changed (78) hide show
  1. {qqmusic_api_python-0.2.1 → qqmusic_api_python-0.3.0}/.gitignore +1 -0
  2. {qqmusic_api_python-0.2.1 → qqmusic_api_python-0.3.0}/PKG-INFO +9 -2
  3. {qqmusic_api_python-0.2.1 → qqmusic_api_python-0.3.0}/README.md +6 -0
  4. {qqmusic_api_python-0.2.1 → qqmusic_api_python-0.3.0}/pyproject.toml +6 -1
  5. qqmusic_api_python-0.3.0/qqmusic_api/__init__.py +27 -0
  6. qqmusic_api_python-0.3.0/qqmusic_api/album.py +58 -0
  7. qqmusic_api_python-0.3.0/qqmusic_api/login.py +440 -0
  8. qqmusic_api_python-0.3.0/qqmusic_api/lyric.py +68 -0
  9. qqmusic_api_python-0.3.0/qqmusic_api/mv.py +79 -0
  10. qqmusic_api_python-0.3.0/qqmusic_api/search.py +138 -0
  11. qqmusic_api_python-0.3.0/qqmusic_api/singer.py +277 -0
  12. qqmusic_api_python-0.3.0/qqmusic_api/song.py +321 -0
  13. qqmusic_api_python-0.3.0/qqmusic_api/songlist.py +49 -0
  14. qqmusic_api_python-0.3.0/qqmusic_api/top.py +34 -0
  15. qqmusic_api_python-0.3.0/qqmusic_api/user.py +206 -0
  16. {qqmusic_api_python-0.2.1 → qqmusic_api_python-0.3.0}/qqmusic_api/utils/common.py +0 -19
  17. {qqmusic_api_python-0.2.1 → qqmusic_api_python-0.3.0}/qqmusic_api/utils/credential.py +5 -31
  18. {qqmusic_api_python-0.2.1 → qqmusic_api_python-0.3.0}/qqmusic_api/utils/device.py +3 -2
  19. qqmusic_api_python-0.3.0/qqmusic_api/utils/network.py +420 -0
  20. {qqmusic_api_python-0.2.1 → qqmusic_api_python-0.3.0}/qqmusic_api/utils/qimei.py +13 -5
  21. qqmusic_api_python-0.3.0/qqmusic_api/utils/session.py +98 -0
  22. qqmusic_api_python-0.3.0/tests/test_album.py +13 -0
  23. qqmusic_api_python-0.3.0/tests/test_login.py +34 -0
  24. {qqmusic_api_python-0.2.1 → qqmusic_api_python-0.3.0}/tests/test_lyric.py +1 -2
  25. qqmusic_api_python-0.3.0/tests/test_mv.py +13 -0
  26. qqmusic_api_python-0.3.0/tests/test_network.py +51 -0
  27. qqmusic_api_python-0.3.0/tests/test_qimei.py +5 -0
  28. qqmusic_api_python-0.3.0/tests/test_session.py +94 -0
  29. qqmusic_api_python-0.3.0/tests/test_singer.py +34 -0
  30. qqmusic_api_python-0.3.0/tests/test_song.py +53 -0
  31. qqmusic_api_python-0.3.0/tests/test_songlist.py +14 -0
  32. qqmusic_api_python-0.3.0/tests/test_top.py +13 -0
  33. qqmusic_api_python-0.3.0/tests/test_user.py +70 -0
  34. qqmusic_api_python-0.2.1/qqmusic_api/__init__.py +0 -31
  35. qqmusic_api_python-0.2.1/qqmusic_api/album.py +0 -92
  36. qqmusic_api_python-0.2.1/qqmusic_api/data/api/album.json +0 -22
  37. qqmusic_api_python-0.2.1/qqmusic_api/data/api/login.json +0 -196
  38. qqmusic_api_python-0.2.1/qqmusic_api/data/api/lyric.json +0 -25
  39. qqmusic_api_python-0.2.1/qqmusic_api/data/api/mv.json +0 -26
  40. qqmusic_api_python-0.2.1/qqmusic_api/data/api/search.json +0 -74
  41. qqmusic_api_python-0.2.1/qqmusic_api/data/api/singer.json +0 -48
  42. qqmusic_api_python-0.2.1/qqmusic_api/data/api/song.json +0 -118
  43. qqmusic_api_python-0.2.1/qqmusic_api/data/api/songlist.json +0 -17
  44. qqmusic_api_python-0.2.1/qqmusic_api/data/api/top.json +0 -20
  45. qqmusic_api_python-0.2.1/qqmusic_api/data/api/user.json +0 -171
  46. qqmusic_api_python-0.2.1/qqmusic_api/data/file_type.json +0 -58
  47. qqmusic_api_python-0.2.1/qqmusic_api/data/search_type.json +0 -11
  48. qqmusic_api_python-0.2.1/qqmusic_api/login.py +0 -421
  49. qqmusic_api_python-0.2.1/qqmusic_api/login_utils.py +0 -141
  50. qqmusic_api_python-0.2.1/qqmusic_api/lyric.py +0 -68
  51. qqmusic_api_python-0.2.1/qqmusic_api/mv.py +0 -118
  52. qqmusic_api_python-0.2.1/qqmusic_api/search.py +0 -145
  53. qqmusic_api_python-0.2.1/qqmusic_api/singer.py +0 -223
  54. qqmusic_api_python-0.2.1/qqmusic_api/song.py +0 -389
  55. qqmusic_api_python-0.2.1/qqmusic_api/songlist.py +0 -58
  56. qqmusic_api_python-0.2.1/qqmusic_api/top.py +0 -52
  57. qqmusic_api_python-0.2.1/qqmusic_api/user.py +0 -277
  58. qqmusic_api_python-0.2.1/qqmusic_api/utils/network.py +0 -282
  59. qqmusic_api_python-0.2.1/qqmusic_api/utils/session.py +0 -162
  60. qqmusic_api_python-0.2.1/qqmusic_api/utils/sync.py +0 -31
  61. qqmusic_api_python-0.2.1/tests/conftest.py +0 -85
  62. qqmusic_api_python-0.2.1/tests/test_album.py +0 -11
  63. qqmusic_api_python-0.2.1/tests/test_login.py +0 -28
  64. qqmusic_api_python-0.2.1/tests/test_mv.py +0 -21
  65. qqmusic_api_python-0.2.1/tests/test_qimei.py +0 -6
  66. qqmusic_api_python-0.2.1/tests/test_singer.py +0 -26
  67. qqmusic_api_python-0.2.1/tests/test_song.py +0 -50
  68. qqmusic_api_python-0.2.1/tests/test_songlist.py +0 -15
  69. qqmusic_api_python-0.2.1/tests/test_top.py +0 -17
  70. qqmusic_api_python-0.2.1/tests/test_user.py +0 -65
  71. {qqmusic_api_python-0.2.1 → qqmusic_api_python-0.3.0}/LICENSE +0 -0
  72. {qqmusic_api_python-0.2.1 → qqmusic_api_python-0.3.0}/qqmusic_api/exceptions/__init__.py +0 -0
  73. {qqmusic_api_python-0.2.1 → qqmusic_api_python-0.3.0}/qqmusic_api/exceptions/api_exception.py +0 -0
  74. {qqmusic_api_python-0.2.1 → qqmusic_api_python-0.3.0}/qqmusic_api/utils/__init__.py +0 -0
  75. {qqmusic_api_python-0.2.1 → qqmusic_api_python-0.3.0}/qqmusic_api/utils/sign.py +0 -0
  76. {qqmusic_api_python-0.2.1 → qqmusic_api_python-0.3.0}/qqmusic_api/utils/tripledes.py +0 -0
  77. {qqmusic_api_python-0.2.1 → qqmusic_api_python-0.3.0}/tests/test_search.py +0 -0
  78. {qqmusic_api_python-0.2.1 → qqmusic_api_python-0.3.0}/tests/test_sign.py +0 -0
@@ -133,6 +133,7 @@ venv/
133
133
  ENV/
134
134
  env.bak/
135
135
  venv.bak/
136
+ .vscode/
136
137
 
137
138
  # Spyder project settings
138
139
  .spyderproject
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qqmusic-api-python
3
- Version: 0.2.1
3
+ Version: 0.3.0
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
@@ -21,7 +21,8 @@ Classifier: Programming Language :: Python :: 3.9
21
21
  Classifier: Programming Language :: Python :: Implementation :: CPython
22
22
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
23
  Requires-Python: >=3.10
24
- Requires-Dist: cryptography<44.0.1,>=44.0.0
24
+ Requires-Dist: aiocache>=0.12.3
25
+ Requires-Dist: cryptography<44.0.2,>=44.0.1
25
26
  Requires-Dist: httpx>=0.27.0
26
27
  Requires-Dist: typing-extensions>=4.12.2
27
28
  Description-Content-Type: text/markdown
@@ -94,6 +95,12 @@ async def main():
94
95
  asyncio.run(main())
95
96
  ```
96
97
 
98
+ ## Web API
99
+ ```
100
+ docker build . -t qq-music-api
101
+ docker run -d -p 8000:8000 qq-music-api
102
+ ```
103
+
97
104
  ## Licence
98
105
 
99
106
  本项目基于 **[MIT License](https://github.com/luren-dc/QQMusicApi?tab=MIT-1-ov-file)** 许可证发行。
@@ -66,6 +66,12 @@ async def main():
66
66
  asyncio.run(main())
67
67
  ```
68
68
 
69
+ ## Web API
70
+ ```
71
+ docker build . -t qq-music-api
72
+ docker run -d -p 8000:8000 qq-music-api
73
+ ```
74
+
69
75
  ## Licence
70
76
 
71
77
  本项目基于 **[MIT License](https://github.com/luren-dc/QQMusicApi?tab=MIT-1-ov-file)** 许可证发行。
@@ -5,9 +5,10 @@ authors = [
5
5
  { name = "Luren", email = "68656403+luren-dc@users.noreply.github.com" },
6
6
  ]
7
7
  dependencies = [
8
- "cryptography>=44.0.0,<44.0.1",
8
+ "cryptography>=44.0.1,<44.0.2",
9
9
  "typing-extensions>=4.12.2",
10
10
  "httpx>=0.27.0",
11
+ "aiocache>=0.12.3",
11
12
  ]
12
13
  requires-python = ">=3.10"
13
14
  readme = "README.md"
@@ -67,6 +68,10 @@ docs = [
67
68
  "docstring-inheritance>=2.2.1",
68
69
  "griffe-modernized-annotations>=1.0.8",
69
70
  ]
71
+ web = [
72
+ "fastapi>=0.115.8",
73
+ "uvicorn>=0.34.0",
74
+ ]
70
75
 
71
76
  [tool.commitizen]
72
77
  name = "cz_gitmoji"
@@ -0,0 +1,27 @@
1
+ import logging
2
+
3
+ from . import album, login, lyric, mv, search, singer, song, songlist, top, user
4
+ from .utils.credential import Credential
5
+ from .utils.session import Session, get_session, set_session
6
+
7
+ __version__ = "0.3.0"
8
+
9
+ logger = logging.getLogger("qqmusicapi")
10
+
11
+
12
+ __all__ = [
13
+ "Credential",
14
+ "Session",
15
+ "album",
16
+ "get_session",
17
+ "login",
18
+ "lyric",
19
+ "mv",
20
+ "search",
21
+ "set_session",
22
+ "singer",
23
+ "song",
24
+ "songlist",
25
+ "top",
26
+ "user",
27
+ ]
@@ -0,0 +1,58 @@
1
+ """专辑相关 API"""
2
+
3
+ from typing import Any, Literal
4
+
5
+ from .utils.network import NO_PROCESSOR, api_request
6
+
7
+
8
+ def get_cover(mid: str, size: Literal[150, 300, 500, 800] = 300) -> str:
9
+ """获取专辑封面链接
10
+
11
+ Args:
12
+ mid: 专辑 mid
13
+ size: 封面大小
14
+
15
+ Returns:
16
+ 封面链接
17
+ """
18
+ if size not in [150, 300, 500, 800]:
19
+ raise ValueError("not supported size")
20
+ return f"https://y.gtimg.cn/music/photo_new/T002R{size}x{size}M000{mid}.jpg"
21
+
22
+
23
+ @api_request("music.musichallAlbum.AlbumInfoServer", "GetAlbumDetail")
24
+ async def get_detail(value: str | int):
25
+ """获取专辑详细信息
26
+
27
+ Args:
28
+ value: 专辑 id 或 mid
29
+ """
30
+ if isinstance(value, int):
31
+ return {"albumId": value}, NO_PROCESSOR
32
+
33
+ return {"albumMId": value}, NO_PROCESSOR
34
+
35
+
36
+ @api_request("music.musichallAlbum.AlbumSongList", "GetAlbumSongList")
37
+ async def get_song(value: str | int, num: int = 10, page: int = 1):
38
+ """获取专辑歌曲
39
+
40
+ Args:
41
+ value: 专辑 id 或 mid
42
+ num: 返回数量
43
+ page: 页码
44
+ """
45
+ params: dict[str, Any] = {
46
+ "begin": num * (page - 1),
47
+ "num": num,
48
+ }
49
+
50
+ def _processor(data: dict[str, Any]) -> list[dict[str, Any]]:
51
+ return [song["songInfo"] for song in data["songList"]]
52
+
53
+ if isinstance(value, int):
54
+ params["albumId"] = value
55
+ else:
56
+ params["albumMid"] = value
57
+
58
+ return params, _processor
@@ -0,0 +1,440 @@
1
+ """登录相关 API"""
2
+
3
+ import mimetypes
4
+ import random
5
+ import re
6
+ from dataclasses import dataclass
7
+ from enum import Enum
8
+ from pathlib import Path
9
+ from time import time
10
+ from typing import Any
11
+ from uuid import uuid4
12
+
13
+ import httpx
14
+
15
+ from .exceptions.api_exception import CredentialExpiredError, LoginError
16
+ from .utils.common import hash33
17
+ from .utils.credential import Credential
18
+ from .utils.network import ApiRequest
19
+ from .utils.session import get_session
20
+
21
+
22
+ async def check_expired(credential: Credential) -> bool:
23
+ """检查凭据是否过期
24
+
25
+ Args:
26
+ credential: 用户凭证
27
+ """
28
+ api = ApiRequest(
29
+ "music.UserInfo.userInfoServer",
30
+ "GetLoginUserInfo",
31
+ params={},
32
+ credential=credential,
33
+ cacheable=False,
34
+ )
35
+
36
+ try:
37
+ await api()
38
+ return False
39
+ except CredentialExpiredError:
40
+ return True
41
+
42
+
43
+ async def refresh_cookies(credential: Credential) -> bool:
44
+ """刷新 Cookies
45
+
46
+ Note:
47
+ 刷新无效 cookie 需要 `refresh_key` 和 `refresh_token` 字段
48
+
49
+ Args:
50
+ credential: 用户凭证
51
+
52
+ Returns:
53
+ 是否刷新成功
54
+ """
55
+ params = {
56
+ "refresh_key": credential.refresh_key,
57
+ "refresh_token": credential.refresh_token,
58
+ "musickey": credential.musickey,
59
+ "musicid": credential.musicid,
60
+ }
61
+
62
+ api = ApiRequest[[], dict[str, Any]](
63
+ "music.login.LoginServer",
64
+ "Login",
65
+ common={"tmeLoginType": str(credential.login_type)},
66
+ params=params,
67
+ credential=credential,
68
+ cacheable=False,
69
+ )
70
+
71
+ try:
72
+ resp = await api()
73
+ c = credential.from_cookies_dict(resp)
74
+ credential.__dict__.update(c.__dict__)
75
+ return True
76
+ except CredentialExpiredError:
77
+ return False
78
+
79
+
80
+ class QRCodeLoginEvents(Enum):
81
+ """二维码登录状态
82
+
83
+ + SCAN: 等待扫描二维码
84
+ + CONF: 已扫码未确认登录
85
+ + TIMEOUT: 二维码已过期
86
+ + DONE: 扫码成功
87
+ + REFUSE: 拒绝登录
88
+ + OTHER: 未知情况
89
+ """
90
+
91
+ DONE = (0, 405)
92
+ SCAN = (66, 408)
93
+ CONF = (67, 404)
94
+ TIMEOUT = (65, None)
95
+ REFUSE = (68, 403)
96
+ OTHER = (None, None)
97
+
98
+ @classmethod
99
+ def get_by_value(cls, value: int):
100
+ """根据传入的值查找对应的枚举成员"""
101
+ for member in cls:
102
+ if value in member.value:
103
+ return member
104
+ return cls.OTHER
105
+
106
+ ""
107
+
108
+
109
+ class PhoneLoginEvents(Enum):
110
+ """手机登录状态
111
+
112
+ + SEND: 发送成功
113
+ + CAPTCHA: 需要滑块验证
114
+ + FREQUENCY: 频繁操作
115
+ + OTHER: 未知情况
116
+ """
117
+
118
+ SEND = 0
119
+ CAPTCHA = 20276
120
+ FREQUENCY = 100001
121
+ OTHER = None
122
+
123
+
124
+ class QRLoginType(Enum):
125
+ """登录类型
126
+
127
+ + QQ: QQ登录
128
+ + WX: 微信登录
129
+ """
130
+
131
+ QQ = "qq"
132
+ WX = "wx"
133
+
134
+
135
+ @dataclass()
136
+ class QR:
137
+ """二维码
138
+
139
+ Attributes:
140
+ data: 二维码图像数据
141
+ qr_type: 二维码类型
142
+ mimetype: 二维码图像类型
143
+ identitfier: 标识符
144
+ """
145
+
146
+ data: bytes
147
+ qr_type: QRLoginType
148
+ mimetype: str
149
+ identifier: str
150
+
151
+ def save(self, path: Path | str = "."):
152
+ """保存二维码
153
+
154
+ Args:
155
+ path: 保存文件夹
156
+ """
157
+ if not self.data:
158
+ return None
159
+ path = Path(path)
160
+ path.mkdir(parents=True, exist_ok=True)
161
+ file_path = (
162
+ path
163
+ / f"{self.qr_type.value}-{uuid4()}{mimetypes.guess_extension(self.mimetype) if self.mimetype else None or '.png'}"
164
+ )
165
+ file_path.write_bytes(self.data)
166
+ return file_path
167
+
168
+
169
+ async def get_qrcode(login_type: QRLoginType) -> QR:
170
+ """获取登录二维码"""
171
+ if login_type == QRLoginType.WX:
172
+ return await _get_wx_qr()
173
+ return await _get_qq_qr()
174
+
175
+
176
+ async def _get_qq_qr() -> QR:
177
+ res = await get_session().get(
178
+ "https://ssl.ptlogin2.qq.com/ptqrshow",
179
+ params={
180
+ "appid": "716027609",
181
+ "e": "2",
182
+ "l": "M",
183
+ "s": "3",
184
+ "d": "72",
185
+ "v": "4",
186
+ "t": str(random.random()),
187
+ "daid": "383",
188
+ "pt_3rd_aid": "100497308",
189
+ },
190
+ headers={"Referer": "https://xui.ptlogin2.qq.com/"},
191
+ )
192
+ qrsig = res.cookies.get("qrsig")
193
+ if not qrsig:
194
+ raise LoginError("[QQLogin] 获取二维码失败")
195
+ return QR(res.read(), QRLoginType.QQ, "image/png", qrsig)
196
+
197
+
198
+ async def _get_wx_qr() -> QR:
199
+ session = get_session()
200
+ res = await session.get(
201
+ "https://open.weixin.qq.com/connect/qrconnect",
202
+ params={
203
+ "appid": "wx48db31d50e334801",
204
+ "redirect_uri": "https://y.qq.com/portal/wx_redirect.html?login_type=2&surl=https://y.qq.com/",
205
+ "response_type": "code",
206
+ "scope": "snsapi_login",
207
+ "state": "STATE",
208
+ "href": "https://y.qq.com/mediastyle/music_v17/src/css/popup_wechat.css#wechat_redirect",
209
+ },
210
+ )
211
+ uuid = re.findall(r"uuid=(.+?)\"", res.text)[0]
212
+ if not uuid:
213
+ raise LoginError("[WXLogin] 获取 uuid 失败")
214
+ qrcode_data = (
215
+ await session.get(
216
+ f"https://open.weixin.qq.com/connect/qrcode/{uuid}",
217
+ headers={"Referer": "https://open.weixin.qq.com/connect/qrconnect"},
218
+ )
219
+ ).read()
220
+ return QR(qrcode_data, QRLoginType.WX, "image/jpeg", uuid)
221
+
222
+
223
+ async def check_qrcode(qrcode: QR) -> tuple[QRCodeLoginEvents, Credential | None]:
224
+ """检查二维码状态"""
225
+ if qrcode.qr_type == QRLoginType.WX:
226
+ return await _check_wx_qr(qrcode)
227
+ return await _check_qq_qr(qrcode)
228
+
229
+
230
+ async def _check_qq_qr(qrcode: QR) -> tuple[QRCodeLoginEvents, Credential | None]:
231
+ qrsig = qrcode.identifier
232
+ try:
233
+ resp = await get_session().get(
234
+ "https://ssl.ptlogin2.qq.com/ptqrlogin",
235
+ params={
236
+ "u1": "https://graph.qq.com/oauth2.0/login_jump",
237
+ "ptqrtoken": hash33(qrsig),
238
+ "ptredirect": "0",
239
+ "h": "1",
240
+ "t": "1",
241
+ "g": "1",
242
+ "from_ui": "1",
243
+ "ptlang": "2052",
244
+ "action": f"0-0-{time() * 1000}",
245
+ "js_ver": "20102616",
246
+ "js_type": "1",
247
+ "pt_uistyle": "40",
248
+ "aid": "716027609",
249
+ "daid": "383",
250
+ "pt_3rd_aid": "100497308",
251
+ "has_onekey": "1",
252
+ },
253
+ headers={"Referer": "https://xui.ptlogin2.qq.com/", "Cookie": f"qrsig={qrsig}"},
254
+ )
255
+ except httpx.HTTPStatusError:
256
+ raise LoginError("[QQLogin] 无效 qrsig")
257
+
258
+ match = re.search(r"ptuiCB\((.*?)\)", resp.text)
259
+ if not match:
260
+ raise LoginError("[QQLogin] 获取二维码状态失败")
261
+
262
+ data = [p.strip("'") for p in match.group(1).split(",")]
263
+ code_str = data[0]
264
+ if code_str.isdigit():
265
+ event = QRCodeLoginEvents.get_by_value(int(code_str))
266
+ if event == QRCodeLoginEvents.DONE:
267
+ sigx = re.findall(r"&ptsigx=(.+?)&s_url", data[2])[0]
268
+ uin = re.findall(r"&uin=(.+?)&service", data[2])[0]
269
+ return event, await _authorize_qq_qr(uin, sigx)
270
+ return event, None
271
+
272
+ return QRCodeLoginEvents.OTHER, None
273
+
274
+
275
+ async def _check_wx_qr(qrcode: QR) -> tuple[QRCodeLoginEvents, Credential | None]:
276
+ uuid = qrcode.identifier
277
+ try:
278
+ resp = await get_session().get(
279
+ "https://lp.open.weixin.qq.com/connect/l/qrconnect",
280
+ params={"uuid": uuid, "_": str(int(time()) * 1000)},
281
+ headers={"Referer": "https://open.weixin.qq.com/"},
282
+ )
283
+ except httpx.TimeoutException:
284
+ return QRCodeLoginEvents.SCAN, None
285
+
286
+ match = re.search(r"window\.wx_errcode=(\d+);window\.wx_code=\'([^\']*)\'", resp.text)
287
+ if not match:
288
+ raise LoginError("[WXLogin] 获取二维码状态失败")
289
+
290
+ wx_errcode = match.group(1)
291
+
292
+ if not wx_errcode.isdigit():
293
+ return QRCodeLoginEvents.OTHER, None
294
+
295
+ event = QRCodeLoginEvents.get_by_value(int(wx_errcode))
296
+
297
+ if event == QRCodeLoginEvents.DONE:
298
+ wx_code = match.group(2)
299
+ if not wx_code:
300
+ raise LoginError("[WXLogin] 获取 code 失败")
301
+
302
+ return event, await _authorize_wx_qr(wx_code)
303
+
304
+ return event, None
305
+
306
+
307
+ async def _authorize_qq_qr(uin: str, sigx: str) -> Credential:
308
+ session = get_session()
309
+ resp = await session.get(
310
+ "https://ssl.ptlogin2.graph.qq.com/check_sig",
311
+ params={
312
+ "uin": uin,
313
+ "pttype": "1",
314
+ "service": "ptqrlogin",
315
+ "nodirect": "0",
316
+ "ptsigx": sigx,
317
+ "s_url": "https://graph.qq.com/oauth2.0/login_jump",
318
+ "ptlang": "2052",
319
+ "ptredirect": "100",
320
+ "aid": "716027609",
321
+ "daid": "383",
322
+ "j_later": "0",
323
+ "low_login_hour": "0",
324
+ "regmaster": "0",
325
+ "pt_login_type": "3",
326
+ "pt_aid": "0",
327
+ "pt_aaid": "16",
328
+ "pt_light": "0",
329
+ "pt_3rd_aid": "100497308",
330
+ },
331
+ headers={"Referer": "https://xui.ptlogin2.qq.com/"},
332
+ )
333
+
334
+ p_skey = resp.cookies.get("p_skey")
335
+
336
+ if not p_skey:
337
+ raise LoginError("[QQLogin] 获取 p_skey 失败")
338
+
339
+ resp = await session.post(
340
+ "https://graph.qq.com/oauth2.0/authorize",
341
+ data={
342
+ "response_type": "code",
343
+ "client_id": "100497308",
344
+ "redirect_uri": "https://y.qq.com/portal/wx_redirect.html?login_type=1&surl=https%3A%252F%252Fy.qq.com%252F",
345
+ "scope": "get_user_info,get_app_friends",
346
+ "state": "state",
347
+ "switch": "",
348
+ "from_ptlogin": "1",
349
+ "src": "1",
350
+ "update_auth": "1",
351
+ "openapi": "1010_1030",
352
+ "g_tk": hash33(p_skey, 5381),
353
+ "auth_time": str(int(time()) * 1000),
354
+ "ui": str(uuid4()),
355
+ },
356
+ )
357
+ location = resp.headers.get("Location", "")
358
+ try:
359
+ code = re.findall(r"(?<=code=)(.+?)(?=&)", location)[0]
360
+ except IndexError:
361
+ raise LoginError("[QQLogin] 获取 code 失败")
362
+ try:
363
+ api = ApiRequest[[], dict[str, Any]](
364
+ "music.login.LoginServer",
365
+ "Login",
366
+ common={"tmeLoginType": "2"},
367
+ params={"code": code},
368
+ cacheable=False,
369
+ )
370
+ return Credential.from_cookies_dict(await api())
371
+ except CredentialExpiredError:
372
+ raise LoginError("[QQLogin] 无法重复鉴权")
373
+
374
+
375
+ async def _authorize_wx_qr(code: str) -> Credential:
376
+ try:
377
+ api = ApiRequest[[], dict[str, Any]](
378
+ "music.login.LoginServer",
379
+ "Login",
380
+ common={"tmeLoginType": "1"},
381
+ params={"code": code, "strAppid": "wx48db31d50e334801"},
382
+ cacheable=False,
383
+ )
384
+ return Credential.from_cookies_dict(await api())
385
+ except CredentialExpiredError:
386
+ raise LoginError("[WXLogin] 无法重复鉴权")
387
+
388
+
389
+ async def send_authcode(phone: int, country_code: int = 86) -> tuple[PhoneLoginEvents, str | None]:
390
+ """发送验证码
391
+
392
+ Args:
393
+ phone: 手机号
394
+ country_code: 国家码
395
+ """
396
+ resp = await ApiRequest[[], dict[str, Any]](
397
+ "music.login.LoginServer",
398
+ "SendPhoneAuthCode",
399
+ common={"tmeLoginMethod": "3"},
400
+ params={
401
+ "tmeAppid": "qqmusic",
402
+ "phoneNo": str(phone),
403
+ "areaCode": str(country_code),
404
+ },
405
+ ignore_code=True,
406
+ cacheable=False,
407
+ )()
408
+
409
+ match resp["code"]:
410
+ case 20276:
411
+ return PhoneLoginEvents.CAPTCHA, resp["data"]["securityURL"]
412
+ case 100001:
413
+ return PhoneLoginEvents.FREQUENCY, None
414
+ case 0:
415
+ return PhoneLoginEvents.SEND, None
416
+ return PhoneLoginEvents.OTHER, resp["data"]["errMsg"]
417
+
418
+
419
+ async def phone_authorize(phone: int, auth_code: int, country_code: int = 86) -> Credential:
420
+ """验证码鉴权
421
+
422
+ Args:
423
+ phone: 手机号
424
+ auth_code: 验证码
425
+ country_code: 国家码
426
+ """
427
+ resp = await ApiRequest[[], dict[str, Any]](
428
+ "music.login.LoginServer",
429
+ "Login",
430
+ common={"tmeLoginMethod": "3", "tmeLoginType": "0"},
431
+ params={"code": str(auth_code), "phoneNo": str(phone), "areaCode": str(country_code), "loginMode": 1},
432
+ ignore_code=True,
433
+ cacheable=False,
434
+ )()
435
+ match resp["code"]:
436
+ case 20271:
437
+ raise LoginError("[PhoneLogin] 验证码错误或已鉴权")
438
+ case 0:
439
+ return Credential.from_cookies_dict(resp["data"])
440
+ raise LoginError("[PhoneLogin] 未知原因导致鉴权失败")
@@ -0,0 +1,68 @@
1
+ """歌词 API"""
2
+
3
+ import re
4
+ from typing import Any
5
+
6
+ from qqmusic_api.utils.network import api_request
7
+
8
+ from .utils.common import qrc_decrypt
9
+
10
+ QRC_PATTERN = re.compile(r'<Lyric_.* LyricType=".*" LyricContent="(?P<content>.*?)"/>', re.DOTALL)
11
+
12
+
13
+ @api_request("music.musichallSong.PlayLyricInfo", "GetPlayLyricInfo")
14
+ async def get_lyric(
15
+ value: str | int,
16
+ qrc: bool = False,
17
+ trans: bool = False,
18
+ roma: bool = False,
19
+ ):
20
+ """获取歌词
21
+
22
+ Args:
23
+ value: 歌曲 id 或 mid
24
+ qrc: 是否返回逐字歌词
25
+ trans: 是否返回翻译歌词
26
+ roma: 是否返回罗马歌词
27
+ """
28
+ params = {
29
+ "crypt": 1,
30
+ "ct": 11,
31
+ "cv": 13020508,
32
+ "lrc_t": 0,
33
+ "qrc": qrc,
34
+ "qrc_t": 0,
35
+ "roma": roma,
36
+ "roma_t": 0,
37
+ "trans": trans,
38
+ "trans_t": 0,
39
+ "type": 1,
40
+ }
41
+
42
+ if isinstance(value, int):
43
+ params["songId"] = value
44
+ else:
45
+ params["songMid"] = value
46
+
47
+ def _processor(data: dict[str, Any]):
48
+ lyric = qrc_decrypt(data["lyric"])
49
+ trans = qrc_decrypt(data["trans"])
50
+ roma = qrc_decrypt(data["roma"])
51
+
52
+ if lyric and qrc:
53
+ m_qrc = QRC_PATTERN.search(lyric)
54
+ if m_qrc and m_qrc.group("content"):
55
+ lyric = m_qrc.group("content")
56
+
57
+ if roma:
58
+ m_roma = QRC_PATTERN.search(roma)
59
+ if m_roma and m_roma.group("content"):
60
+ roma = m_roma.group("content")
61
+
62
+ return {
63
+ "lyric": lyric,
64
+ "trans": trans,
65
+ "roma": roma,
66
+ }
67
+
68
+ return params, _processor