qqmusic-api-python 0.3.6__tar.gz → 0.4.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 (47) hide show
  1. {qqmusic_api_python-0.3.6 → qqmusic_api_python-0.4.0}/PKG-INFO +13 -12
  2. {qqmusic_api_python-0.3.6 → qqmusic_api_python-0.4.0}/README.md +11 -11
  3. {qqmusic_api_python-0.3.6 → qqmusic_api_python-0.4.0}/pyproject.toml +1 -0
  4. {qqmusic_api_python-0.3.6 → qqmusic_api_python-0.4.0}/qqmusic_api/__init__.py +9 -1
  5. {qqmusic_api_python-0.3.6 → qqmusic_api_python-0.4.0}/qqmusic_api/comment.py +29 -65
  6. {qqmusic_api_python-0.3.6 → qqmusic_api_python-0.4.0}/qqmusic_api/login.py +119 -7
  7. {qqmusic_api_python-0.3.6 → qqmusic_api_python-0.4.0}/qqmusic_api/mv.py +1 -1
  8. {qqmusic_api_python-0.3.6 → qqmusic_api_python-0.4.0}/qqmusic_api/search.py +4 -4
  9. {qqmusic_api_python-0.3.6 → qqmusic_api_python-0.4.0}/qqmusic_api/singer.py +1 -1
  10. {qqmusic_api_python-0.3.6 → qqmusic_api_python-0.4.0}/qqmusic_api/song.py +6 -11
  11. {qqmusic_api_python-0.3.6 → qqmusic_api_python-0.4.0}/qqmusic_api/songlist.py +5 -5
  12. {qqmusic_api_python-0.3.6 → qqmusic_api_python-0.4.0}/qqmusic_api/user.py +12 -12
  13. qqmusic_api_python-0.4.0/qqmusic_api/utils/mqtt.py +347 -0
  14. {qqmusic_api_python-0.3.6 → qqmusic_api_python-0.4.0}/qqmusic_api/utils/network.py +20 -69
  15. {qqmusic_api_python-0.3.6 → qqmusic_api_python-0.4.0}/qqmusic_api/utils/session.py +0 -30
  16. qqmusic_api_python-0.4.0/tests/test_comment.py +38 -0
  17. {qqmusic_api_python-0.3.6 → qqmusic_api_python-0.4.0}/tests/test_login.py +10 -2
  18. {qqmusic_api_python-0.3.6 → qqmusic_api_python-0.4.0}/tests/test_song.py +3 -1
  19. qqmusic_api_python-0.3.6/tests/test_comment.py +0 -19
  20. {qqmusic_api_python-0.3.6 → qqmusic_api_python-0.4.0}/.gitignore +0 -0
  21. {qqmusic_api_python-0.3.6 → qqmusic_api_python-0.4.0}/LICENSE +0 -0
  22. {qqmusic_api_python-0.3.6 → qqmusic_api_python-0.4.0}/qqmusic_api/album.py +0 -0
  23. {qqmusic_api_python-0.3.6 → qqmusic_api_python-0.4.0}/qqmusic_api/exceptions/__init__.py +0 -0
  24. {qqmusic_api_python-0.3.6 → qqmusic_api_python-0.4.0}/qqmusic_api/exceptions/api_exception.py +0 -0
  25. {qqmusic_api_python-0.3.6 → qqmusic_api_python-0.4.0}/qqmusic_api/lyric.py +0 -0
  26. {qqmusic_api_python-0.3.6 → qqmusic_api_python-0.4.0}/qqmusic_api/recommend.py +0 -0
  27. {qqmusic_api_python-0.3.6 → qqmusic_api_python-0.4.0}/qqmusic_api/top.py +0 -0
  28. {qqmusic_api_python-0.3.6 → qqmusic_api_python-0.4.0}/qqmusic_api/utils/__init__.py +0 -0
  29. {qqmusic_api_python-0.3.6 → qqmusic_api_python-0.4.0}/qqmusic_api/utils/common.py +0 -0
  30. {qqmusic_api_python-0.3.6 → qqmusic_api_python-0.4.0}/qqmusic_api/utils/credential.py +0 -0
  31. {qqmusic_api_python-0.3.6 → qqmusic_api_python-0.4.0}/qqmusic_api/utils/device.py +0 -0
  32. {qqmusic_api_python-0.3.6 → qqmusic_api_python-0.4.0}/qqmusic_api/utils/qimei.py +0 -0
  33. {qqmusic_api_python-0.3.6 → qqmusic_api_python-0.4.0}/qqmusic_api/utils/sign.py +0 -0
  34. {qqmusic_api_python-0.3.6 → qqmusic_api_python-0.4.0}/qqmusic_api/utils/tripledes.py +0 -0
  35. {qqmusic_api_python-0.3.6 → qqmusic_api_python-0.4.0}/tests/test_album.py +0 -0
  36. {qqmusic_api_python-0.3.6 → qqmusic_api_python-0.4.0}/tests/test_lyric.py +0 -0
  37. {qqmusic_api_python-0.3.6 → qqmusic_api_python-0.4.0}/tests/test_mv.py +0 -0
  38. {qqmusic_api_python-0.3.6 → qqmusic_api_python-0.4.0}/tests/test_qimei.py +0 -0
  39. {qqmusic_api_python-0.3.6 → qqmusic_api_python-0.4.0}/tests/test_recommend.py +0 -0
  40. {qqmusic_api_python-0.3.6 → qqmusic_api_python-0.4.0}/tests/test_search.py +0 -0
  41. {qqmusic_api_python-0.3.6 → qqmusic_api_python-0.4.0}/tests/test_session.py +0 -0
  42. {qqmusic_api_python-0.3.6 → qqmusic_api_python-0.4.0}/tests/test_sign.py +0 -0
  43. {qqmusic_api_python-0.3.6 → qqmusic_api_python-0.4.0}/tests/test_singer.py +0 -0
  44. {qqmusic_api_python-0.3.6 → qqmusic_api_python-0.4.0}/tests/test_songlist.py +0 -0
  45. {qqmusic_api_python-0.3.6 → qqmusic_api_python-0.4.0}/tests/test_top.py +0 -0
  46. {qqmusic_api_python-0.3.6 → qqmusic_api_python-0.4.0}/tests/test_user.py +0 -0
  47. {qqmusic_api_python-0.3.6 → qqmusic_api_python-0.4.0}/web/README.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qqmusic-api-python
3
- Version: 0.3.6
3
+ Version: 0.4.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
@@ -23,6 +23,7 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
23
  Requires-Python: >=3.10
24
24
  Requires-Dist: aiocache>=0.12.3
25
25
  Requires-Dist: cryptography<45.0.6,>=45.0.5
26
+ Requires-Dist: httpx-ws>=0.8.2
26
27
  Requires-Dist: httpx[http2]>=0.27.0
27
28
  Requires-Dist: orjson>=3.10.15
28
29
  Requires-Dist: typing-extensions>=4.12.2
@@ -30,19 +31,19 @@ Description-Content-Type: text/markdown
30
31
 
31
32
  <div align="center">
32
33
  <a>
33
- <img src="https://socialify.git.ci/luren-dc/QQMusicApi/image?description=1&font=Source%20Code%20Pro&language=1&logo=https%3A%2F%2Fy.qq.com%2Fmediastyle%2Fmod%2Fmobile%2Fimg%2Flogo.svg&name=1&pattern=Overlapping%20Hexagons&theme=Auto">
34
+ <img src="https://socialify.git.ci/l-1124/QQMusicApi/image?description=1&font=Source%20Code%20Pro&language=1&logo=https%3A%2F%2Fy.qq.com%2Fmediastyle%2Fmod%2Fmobile%2Fimg%2Flogo.svg&name=1&pattern=Overlapping%20Hexagons&theme=Auto">
34
35
  </a>
35
36
  <a href="https://www.python.org">
36
37
  <img src="https://img.shields.io/badge/Python-3.10|3.11|3.12|3.13-blue" alt="Python">
37
38
  </a>
38
- <a href="https://github.com/luren-dc/QQMusicApi?tab=MIT-1-ov-file">
39
- <img src="https://img.shields.io/github/license/luren-dc/QQMusicApi" alt="GitHub license">
39
+ <a href="https://github.com/l-1124/QQMusicApi?tab=MIT-1-ov-file">
40
+ <img src="https://img.shields.io/github/license/l-1124/QQMusicApi" alt="GitHub license">
40
41
  </a>
41
- <a href="https://github.com/luren-dc/QQMusicApi/stargazers">
42
- <img src="https://img.shields.io/github/stars/luren-dc/QQMusicApi?color=yellow&label=Github%20Stars" alt="STARS">
42
+ <a href="https://github.com/l-1124/QQMusicApi/stargazers">
43
+ <img src="https://img.shields.io/github/stars/l-1124/QQMusicApi?color=yellow&label=Github%20Stars" alt="STARS">
43
44
  </a>
44
- <a href="https://github.com/luren-dc/QQMusicApi/actions/workflows/testing.yml">
45
- <img src="https://github.com/luren-dc/QQMusicApi/actions/workflows/testing.yml/badge.svg?branch=main" alt="Testing">
45
+ <a href="https://github.com/l-1124/QQMusicApi/actions/workflows/testing.yml">
46
+ <img src="https://github.com/l-1124/QQMusicApi/actions/workflows/testing.yml/badge.svg?branch=main" alt="Testing">
46
47
  </a>
47
48
  </div>
48
49
 
@@ -53,9 +54,9 @@ Description-Content-Type: text/markdown
53
54
  >
54
55
  > **音乐平台不易,请尊重版权,支持正版。**
55
56
 
56
- **文档**: <a href="https://luren-dc.github.io/QQMusicApi" target="_blank">https://luren-dc.github.io/QQMusicApi</a>
57
+ **文档**: <a href="https://l-1124.github.io/QQMusicApi" target="_blank">https://l-1124.github.io/QQMusicApi</a>
57
58
 
58
- **源代码**: <a href="https://github.com/luren-dc/QQMusicApi" target="_blank">https://github.com/luren-dc/QQMusicApi</a>
59
+ **源代码**: <a href="https://github.com/l-1124/QQMusicApi" target="_blank">https://github.com/l-1124/QQMusicApi</a>
59
60
 
60
61
  ## 介绍
61
62
 
@@ -102,7 +103,7 @@ asyncio.run(main())
102
103
 
103
104
  ## License
104
105
 
105
- 本项目基于 **[MIT License](https://github.com/luren-dc/QQMusicApi?tab=MIT-1-ov-file)** 许可证发行。
106
+ 本项目基于 **[MIT License](https://github.com/l-1124/QQMusicApi?tab=MIT-1-ov-file)** 许可证发行。
106
107
 
107
108
  ## 免责声明
108
109
 
@@ -110,4 +111,4 @@ asyncio.run(main())
110
111
 
111
112
  ## 贡献者
112
113
 
113
- [![Contributor](https://contrib.rocks/image?repo=luren-dc/QQMusicApi)](https://github.com/luren-dc/QQMusicApi/graphs/contributors)
114
+ [![Contributor](https://contrib.rocks/image?repo=l-1124/QQMusicApi)](https://github.com/l-1124/QQMusicApi/graphs/contributors)
@@ -1,18 +1,18 @@
1
1
  <div align="center">
2
2
  <a>
3
- <img src="https://socialify.git.ci/luren-dc/QQMusicApi/image?description=1&font=Source%20Code%20Pro&language=1&logo=https%3A%2F%2Fy.qq.com%2Fmediastyle%2Fmod%2Fmobile%2Fimg%2Flogo.svg&name=1&pattern=Overlapping%20Hexagons&theme=Auto">
3
+ <img src="https://socialify.git.ci/l-1124/QQMusicApi/image?description=1&font=Source%20Code%20Pro&language=1&logo=https%3A%2F%2Fy.qq.com%2Fmediastyle%2Fmod%2Fmobile%2Fimg%2Flogo.svg&name=1&pattern=Overlapping%20Hexagons&theme=Auto">
4
4
  </a>
5
5
  <a href="https://www.python.org">
6
6
  <img src="https://img.shields.io/badge/Python-3.10|3.11|3.12|3.13-blue" alt="Python">
7
7
  </a>
8
- <a href="https://github.com/luren-dc/QQMusicApi?tab=MIT-1-ov-file">
9
- <img src="https://img.shields.io/github/license/luren-dc/QQMusicApi" alt="GitHub license">
8
+ <a href="https://github.com/l-1124/QQMusicApi?tab=MIT-1-ov-file">
9
+ <img src="https://img.shields.io/github/license/l-1124/QQMusicApi" alt="GitHub license">
10
10
  </a>
11
- <a href="https://github.com/luren-dc/QQMusicApi/stargazers">
12
- <img src="https://img.shields.io/github/stars/luren-dc/QQMusicApi?color=yellow&label=Github%20Stars" alt="STARS">
11
+ <a href="https://github.com/l-1124/QQMusicApi/stargazers">
12
+ <img src="https://img.shields.io/github/stars/l-1124/QQMusicApi?color=yellow&label=Github%20Stars" alt="STARS">
13
13
  </a>
14
- <a href="https://github.com/luren-dc/QQMusicApi/actions/workflows/testing.yml">
15
- <img src="https://github.com/luren-dc/QQMusicApi/actions/workflows/testing.yml/badge.svg?branch=main" alt="Testing">
14
+ <a href="https://github.com/l-1124/QQMusicApi/actions/workflows/testing.yml">
15
+ <img src="https://github.com/l-1124/QQMusicApi/actions/workflows/testing.yml/badge.svg?branch=main" alt="Testing">
16
16
  </a>
17
17
  </div>
18
18
 
@@ -23,9 +23,9 @@
23
23
  >
24
24
  > **音乐平台不易,请尊重版权,支持正版。**
25
25
 
26
- **文档**: <a href="https://luren-dc.github.io/QQMusicApi" target="_blank">https://luren-dc.github.io/QQMusicApi</a>
26
+ **文档**: <a href="https://l-1124.github.io/QQMusicApi" target="_blank">https://l-1124.github.io/QQMusicApi</a>
27
27
 
28
- **源代码**: <a href="https://github.com/luren-dc/QQMusicApi" target="_blank">https://github.com/luren-dc/QQMusicApi</a>
28
+ **源代码**: <a href="https://github.com/l-1124/QQMusicApi" target="_blank">https://github.com/l-1124/QQMusicApi</a>
29
29
 
30
30
  ## 介绍
31
31
 
@@ -72,7 +72,7 @@ asyncio.run(main())
72
72
 
73
73
  ## License
74
74
 
75
- 本项目基于 **[MIT License](https://github.com/luren-dc/QQMusicApi?tab=MIT-1-ov-file)** 许可证发行。
75
+ 本项目基于 **[MIT License](https://github.com/l-1124/QQMusicApi?tab=MIT-1-ov-file)** 许可证发行。
76
76
 
77
77
  ## 免责声明
78
78
 
@@ -80,4 +80,4 @@ asyncio.run(main())
80
80
 
81
81
  ## 贡献者
82
82
 
83
- [![Contributor](https://contrib.rocks/image?repo=luren-dc/QQMusicApi)](https://github.com/luren-dc/QQMusicApi/graphs/contributors)
83
+ [![Contributor](https://contrib.rocks/image?repo=l-1124/QQMusicApi)](https://github.com/l-1124/QQMusicApi/graphs/contributors)
@@ -10,6 +10,7 @@ dependencies = [
10
10
  "httpx[http2]>=0.27.0",
11
11
  "aiocache>=0.12.3",
12
12
  "orjson>=3.10.15",
13
+ "httpx-ws>=0.8.2",
13
14
  ]
14
15
  requires-python = ">=3.10"
15
16
  readme = "README.md"
@@ -1,13 +1,21 @@
1
1
  import logging
2
+ import os
3
+ import sys
2
4
 
3
5
  from . import album, comment, login, lyric, mv, search, singer, song, songlist, top, user
4
6
  from .utils.credential import Credential
5
7
  from .utils.session import Session, get_session, set_session
6
8
 
7
- __version__ = "0.3.6"
9
+ __version__ = "0.4.0"
8
10
 
9
11
  logger = logging.getLogger("qqmusicapi")
10
12
 
13
+ # Change to the "Selector" event loop if platform is Windows
14
+ if sys.platform.lower() == "win32" or os.name.lower() == "nt":
15
+ from asyncio import WindowsSelectorEventLoopPolicy, set_event_loop_policy
16
+
17
+ set_event_loop_policy(WindowsSelectorEventLoopPolicy())
18
+
11
19
 
12
20
  __all__ = [
13
21
  "Credential",
@@ -1,8 +1,6 @@
1
1
  """评论 API"""
2
2
 
3
- from typing import Any, cast
4
-
5
- from .utils.network import api_request
3
+ from .utils.network import NO_PROCESSOR, api_request
6
4
 
7
5
 
8
6
  @api_request("music.globalComment.CommentCountSrv", "GetCmCount")
@@ -18,65 +16,7 @@ async def get_comment_count(biz_id: str):
18
16
  "biz_type": 1,
19
17
  "biz_sub_type": 2,
20
18
  },
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
19
+ }, NO_PROCESSOR
80
20
 
81
21
 
82
22
  @api_request("music.globalComment.CommentRead", "GetHotCommentList")
@@ -105,7 +45,7 @@ async def get_hot_comments(
105
45
  "PicEnable": 1,
106
46
  }
107
47
 
108
- return params, _processor
48
+ return params, NO_PROCESSOR
109
49
 
110
50
 
111
51
  @api_request("music.globalComment.CommentRead", "GetNewCommentList")
@@ -140,7 +80,7 @@ async def get_new_comments(
140
80
  "AudioEnable": 1,
141
81
  }
142
82
 
143
- return params, _processor
83
+ return params, NO_PROCESSOR
144
84
 
145
85
 
146
86
  @api_request("music.globalComment.CommentRead", "GetRecCommentList")
@@ -180,4 +120,28 @@ async def get_recommend_comments(
180
120
  "AudioEnable": 1,
181
121
  }
182
122
 
183
- return params, _processor
123
+ return params, NO_PROCESSOR
124
+
125
+
126
+ @api_request("music.globalComment.SongTsComment", "GetSongTsCmList")
127
+ async def get_moment_comments(
128
+ biz_id: str,
129
+ page_size: int = 15,
130
+ last_comment_seq_no: str = "",
131
+ ):
132
+ """获取时刻评论
133
+
134
+ Args:
135
+ biz_id: 歌曲 ID
136
+ page_size: 每页数量
137
+ last_comment_seq_no: 上一页最后一条评论ID
138
+ """
139
+ params = {
140
+ "LastPos": last_comment_seq_no,
141
+ "HashTagID": "",
142
+ "SeekTs": -1,
143
+ "Size": page_size,
144
+ "BizType": 1,
145
+ "BizId": biz_id,
146
+ }
147
+ return params, NO_PROCESSOR
@@ -3,6 +3,7 @@
3
3
  import mimetypes
4
4
  import random
5
5
  import re
6
+ from collections.abc import AsyncGenerator
6
7
  from dataclasses import dataclass
7
8
  from enum import Enum
8
9
  from pathlib import Path
@@ -15,6 +16,8 @@ import httpx
15
16
  from .exceptions.api_exception import CredentialExpiredError, LoginError, ResponseCodeError
16
17
  from .utils.common import hash33
17
18
  from .utils.credential import Credential
19
+ from .utils.mqtt import Client as MqttClient
20
+ from .utils.mqtt import MqttRedirectError, PropertyId
18
21
  from .utils.network import ApiRequest
19
22
  from .utils.session import get_session
20
23
 
@@ -30,7 +33,6 @@ async def check_expired(credential: Credential) -> bool:
30
33
  "GetLoginUserInfo",
31
34
  params={},
32
35
  credential=credential,
33
- cacheable=False,
34
36
  )
35
37
 
36
38
  try:
@@ -65,7 +67,6 @@ async def refresh_cookies(credential: Credential) -> bool:
65
67
  common={"tmeLoginType": str(credential.login_type)},
66
68
  params=params,
67
69
  credential=credential,
68
- cacheable=False,
69
70
  )
70
71
 
71
72
  try:
@@ -124,10 +125,12 @@ class QRLoginType(Enum):
124
125
 
125
126
  + QQ: QQ登录
126
127
  + WX: 微信登录
128
+ + MOBILE: 手机客户端登录
127
129
  """
128
130
 
129
131
  QQ = "qq"
130
132
  WX = "wx"
133
+ MOBILE = "mobile"
131
134
 
132
135
 
133
136
  @dataclass()
@@ -138,7 +141,7 @@ class QR:
138
141
  data: 二维码图像数据
139
142
  qr_type: 二维码类型
140
143
  mimetype: 二维码图像类型
141
- identitfier: 标识符
144
+ identifier: 标识符
142
145
  """
143
146
 
144
147
  data: bytes
@@ -164,10 +167,13 @@ class QR:
164
167
  return file_path
165
168
 
166
169
 
170
+ # 修改 qqmusic_api/login.py 中的 get_qrcode 函数
167
171
  async def get_qrcode(login_type: QRLoginType) -> QR:
168
172
  """获取登录二维码"""
169
173
  if login_type == QRLoginType.WX:
170
174
  return await _get_wx_qr()
175
+ if login_type == QRLoginType.MOBILE:
176
+ return await _get_mobile_qr()
171
177
  return await _get_qq_qr()
172
178
 
173
179
 
@@ -218,6 +224,27 @@ async def _get_wx_qr() -> QR:
218
224
  return QR(qrcode_data, QRLoginType.WX, "image/jpeg", uuid)
219
225
 
220
226
 
227
+ async def _get_mobile_qr() -> QR:
228
+ import base64
229
+
230
+ res = await ApiRequest[[], dict[str, Any]](
231
+ "music.login.LoginServer",
232
+ "CreateQRCode",
233
+ params={
234
+ "tmeAppID": "qqmusic",
235
+ "ct": 11,
236
+ "cv": 13020508,
237
+ },
238
+ )()
239
+
240
+ return QR(
241
+ data=base64.b64decode(res["qrcode"].split(",")[-1]),
242
+ qr_type=QRLoginType.MOBILE,
243
+ mimetype="image/png",
244
+ identifier=res["qrcodeID"],
245
+ )
246
+
247
+
221
248
  async def check_qrcode(qrcode: QR) -> tuple[QRCodeLoginEvents, Credential | None]:
222
249
  """检查二维码状态"""
223
250
  if qrcode.qr_type == QRLoginType.WX:
@@ -302,6 +329,95 @@ async def _check_wx_qr(qrcode: QR) -> tuple[QRCodeLoginEvents, Credential | None
302
329
  return event, None
303
330
 
304
331
 
332
+ async def check_mobile_qr(qrcode: QR) -> AsyncGenerator[tuple[QRCodeLoginEvents, Credential | None], None]: # noqa: C901
333
+ """检查手机客户端登录二维码状态
334
+
335
+ Args:
336
+ qrcode: 二维码对象
337
+ """
338
+ if qrcode.qr_type != QRLoginType.MOBILE:
339
+ raise ValueError(f"不支持{qrcode.qr_type}类型的二维码")
340
+ client_id = f"{int(time() * 1000)}{random.randint(1000, 9999)}"
341
+ client = MqttClient(client_id=client_id, host="mu.y.qq.com", port=443, path="/ws/handshake", keep_alive=45)
342
+ async with client:
343
+ max_redirects = 3
344
+ for attempt in range(max_redirects + 1):
345
+ try:
346
+ await client.connect(
347
+ properties={
348
+ PropertyId.AUTH_METHOD: "pass",
349
+ PropertyId.USER_PROPERTY: [
350
+ ("tmeAppID", "qqmusic"),
351
+ ("business", "management"),
352
+ ("hashTag", qrcode.identifier),
353
+ ("clientTag", "management.user"),
354
+ ("userID", qrcode.identifier),
355
+ ],
356
+ },
357
+ headers={
358
+ "Origin": "https://y.qq.com",
359
+ "Referer": "https://y.qq.com/",
360
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36",
361
+ },
362
+ )
363
+ break
364
+ except MqttRedirectError as e:
365
+ if attempt == max_redirects:
366
+ raise LoginError("[MobileLogin] 重定向次数过多") from e
367
+
368
+ client.path = f"/ws/handshake/{e.new_address}"
369
+ continue
370
+ await client.subscribe(
371
+ f"management.qrcode_login/{qrcode.identifier}",
372
+ properties={
373
+ PropertyId.USER_PROPERTY: [
374
+ ("authorization", "tmelogin"),
375
+ ("pubsub", "unicast"),
376
+ ]
377
+ },
378
+ )
379
+ yield QRCodeLoginEvents.SCAN, None
380
+ async for msg in client.messages():
381
+ event_type = msg.properties.get("type")
382
+
383
+ match event_type:
384
+ case "scanned":
385
+ yield QRCodeLoginEvents.CONF, None
386
+
387
+ case "cookies":
388
+ data = msg.json
389
+ if data:
390
+ cookies: dict[str, Any] = {
391
+ k: v.get("value") if isinstance(v, dict) else v for k, v in data.get("cookies", {}).items()
392
+ }
393
+ res = await ApiRequest[[], dict[str, Any]](
394
+ "music.login.LoginServer",
395
+ "Login",
396
+ params={
397
+ "musicid": int(cookies.get("qqmusic_uin", 0)),
398
+ "qrCodeID": qrcode.identifier,
399
+ "token": cookies.get("qqmusic_key", ""),
400
+ },
401
+ common={"tmeLoginType": "6"},
402
+ )()
403
+
404
+ yield QRCodeLoginEvents.DONE, Credential.from_cookies_dict(res)
405
+ else:
406
+ yield QRCodeLoginEvents.OTHER, None
407
+ break
408
+ case "canceled":
409
+ yield QRCodeLoginEvents.REFUSE, None
410
+ break
411
+ case "timeout":
412
+ yield QRCodeLoginEvents.TIMEOUT, None
413
+ break
414
+ case "loginFailed":
415
+ yield QRCodeLoginEvents.OTHER, None
416
+ break
417
+ case _:
418
+ pass
419
+
420
+
305
421
  async def _authorize_qq_qr(uin: str, sigx: str) -> Credential:
306
422
  session = get_session()
307
423
  resp = await session.get(
@@ -363,7 +479,6 @@ async def _authorize_qq_qr(uin: str, sigx: str) -> Credential:
363
479
  "QQLogin",
364
480
  common={"tmeLoginType": "2"},
365
481
  params={"code": code},
366
- cacheable=False,
367
482
  )
368
483
  return Credential.from_cookies_dict(await api())
369
484
  except CredentialExpiredError:
@@ -381,7 +496,6 @@ async def _authorize_wx_qr(code: str) -> Credential:
381
496
  "Login",
382
497
  common={"tmeLoginType": "1"},
383
498
  params={"code": code, "strAppid": "wx48db31d50e334801"},
384
- cacheable=False,
385
499
  )
386
500
  return Credential.from_cookies_dict(await api())
387
501
  except CredentialExpiredError:
@@ -405,7 +519,6 @@ async def send_authcode(phone: int, country_code: int = 86) -> tuple[PhoneLoginE
405
519
  "areaCode": str(country_code),
406
520
  },
407
521
  ignore_code=True,
408
- cacheable=False,
409
522
  )()
410
523
 
411
524
  match resp["code"]:
@@ -432,7 +545,6 @@ async def phone_authorize(phone: int, auth_code: int, country_code: int = 86) ->
432
545
  common={"tmeLoginMethod": "3", "tmeLoginType": "0"},
433
546
  params={"code": str(auth_code), "phoneNo": str(phone), "areaCode": str(country_code), "loginMode": 1},
434
547
  ignore_code=True,
435
- cacheable=False,
436
548
  )()
437
549
  match resp["code"]:
438
550
  case 20274:
@@ -43,7 +43,7 @@ async def get_detail(vids: list[str]):
43
43
  }, NO_PROCESSOR
44
44
 
45
45
 
46
- @api_request("music.stream.MvUrlProxy", "GetMvUrls", exclude_params=["guid"])
46
+ @api_request("music.stream.MvUrlProxy", "GetMvUrls")
47
47
  async def get_mv_urls(vids: list[str]):
48
48
  """获取 MV 播放链接
49
49
 
@@ -33,13 +33,13 @@ class SearchType(Enum):
33
33
  AUDIO = 18
34
34
 
35
35
 
36
- @api_request("music.musicsearch.HotkeyService", "GetHotkeyForQQMusicMobile", exclude_params=["search_id"])
36
+ @api_request("music.musicsearch.HotkeyService", "GetHotkeyForQQMusicMobile")
37
37
  async def hotkey():
38
38
  """获取热搜词"""
39
39
  return {"search_id": get_searchID()}, NO_PROCESSOR
40
40
 
41
41
 
42
- @api_request("music.smartboxCgi.SmartBoxCgi", "GetSmartBoxResult", exclude_params=["search_id"])
42
+ @api_request("music.smartboxCgi.SmartBoxCgi", "GetSmartBoxResult")
43
43
  async def complete(keyword: str):
44
44
  """搜索词补全
45
45
 
@@ -70,7 +70,7 @@ async def quick_search(keyword: str) -> dict[str, Any]:
70
70
  return resp.json()["data"]
71
71
 
72
72
 
73
- @api_request("music.adaptor.SearchAdaptor", "do_search_v2", exclude_params=["searchid"])
73
+ @api_request("music.adaptor.SearchAdaptor", "do_search_v2")
74
74
  async def general_search(
75
75
  keyword: str,
76
76
  page: int = 1,
@@ -94,7 +94,7 @@ async def general_search(
94
94
  }, NO_PROCESSOR
95
95
 
96
96
 
97
- @api_request("music.search.SearchCgiService", "DoSearchForQQMusicMobile", exclude_params=["searchid"])
97
+ @api_request("music.search.SearchCgiService", "DoSearchForQQMusicMobile")
98
98
  async def search_by_type(
99
99
  keyword: str,
100
100
  search_type: SearchType = SearchType.SONG,
@@ -237,7 +237,7 @@ async def get_tab_detail(mid: str, tab_type: TabType, page: int = 1, num: int =
237
237
 
238
238
  def _processor(data: dict[str, Any]) -> list[dict[str, Any]]:
239
239
  data = data[tab_type.tab_name]
240
- return data.get("List", data.get("VideoList", data.get("AlbumList", data)))
240
+ return data.get("List") or data.get("VideoList") or data.get("AlbumList") or []
241
241
 
242
242
  return params, _processor
243
243
 
@@ -159,14 +159,16 @@ async def get_song_urls(
159
159
  urls = {}
160
160
  data = res["midurlinfo"]
161
161
  for info in data:
162
- song_url = domain + info["wifiurl"] if info["wifiurl"] else ""
162
+ song_url = (
163
+ domain + info.get("purl") or info.get("wifiurl") if info.get("purl") or info.get("wifiurl") else ""
164
+ )
163
165
  if not encrypted:
164
166
  urls[info["songmid"]] = song_url
165
167
  else:
166
168
  urls[info["songmid"]] = (song_url, info["ekey"])
167
169
  return urls
168
170
 
169
- rg = RequestGroup(credential=credential)
171
+ rg = RequestGroup(common={"ct": "19"}, credential=credential)
170
172
  for mid in mid_list:
171
173
  # 构造请求参数
172
174
  file_name = [f"{file_type.s}{_}{_}{file_type.e}" for _ in mid]
@@ -181,8 +183,6 @@ async def get_song_urls(
181
183
  api_data[1],
182
184
  params=params,
183
185
  credential=credential,
184
- exclude_params=["guid"],
185
- cacheable=False,
186
186
  )
187
187
  req.processor = _processor
188
188
  rg.add_request(req)
@@ -194,12 +194,7 @@ async def get_song_urls(
194
194
  return result
195
195
 
196
196
 
197
- @api_request(
198
- "music.vkey.GetVkey",
199
- "UrlGetVkey",
200
- exclude_params=["guid"],
201
- cacheable=False,
202
- )
197
+ @api_request("music.vkey.GetVkey", "UrlGetVkey")
203
198
  async def get_try_url(mid: str, vs: str):
204
199
  """获取试听文件链接
205
200
 
@@ -217,7 +212,7 @@ async def get_try_url(mid: str, vs: str):
217
212
  "guid": get_guid(),
218
213
  "songmid": [mid],
219
214
  "songtype": [1],
220
- }, lambda res: f"https://isure.stream.qqmusic.qq.com/{url}" if (url := res["midurlinfo"][0]["wifiurl"]) else ""
215
+ }, lambda res: f"https://isure.stream.qqmusic.qq.com/{url}" if (url := res["midurlinfo"][0]["wifiurl"]) else None
221
216
 
222
217
 
223
218
  @api_request("music.pf_song_detail_svr", "get_song_detail_yqq")
@@ -78,9 +78,9 @@ async def get_songlist(
78
78
  return songs
79
79
 
80
80
 
81
- @api_request("music.musicasset.PlaylistBaseWrite", "AddPlaylist", verify=True, cacheable=False)
81
+ @api_request("music.musicasset.PlaylistBaseWrite", "AddPlaylist", verify=True)
82
82
  async def create(dirname: str, *, credential: Credential | None = None):
83
- """添加歌单, 重名会在名称后面添加时间戳
83
+ """创建歌单, 重名会在名称后面添加时间戳
84
84
 
85
85
  Args:
86
86
  dirname: 歌单名称
@@ -94,7 +94,7 @@ async def create(dirname: str, *, credential: Credential | None = None):
94
94
  }, lambda data: cast(dict[str, Any], data["result"])
95
95
 
96
96
 
97
- @api_request("music.musicasset.PlaylistBaseWrite", "DelPlaylist", verify=True, cacheable=False)
97
+ @api_request("music.musicasset.PlaylistBaseWrite", "DelPlaylist", verify=True)
98
98
  async def delete(dirid: int, *, credential: Credential | None = None):
99
99
  """删除歌单
100
100
 
@@ -110,7 +110,7 @@ async def delete(dirid: int, *, credential: Credential | None = None):
110
110
  }, lambda data: data["result"]["dirId"] == dirid
111
111
 
112
112
 
113
- @api_request("music.musicasset.PlaylistDetailWrite", "AddSonglist", verify=True, cacheable=False)
113
+ @api_request("music.musicasset.PlaylistDetailWrite", "AddSonglist", verify=True)
114
114
  async def add_songs(dirid: int = 1, song_ids: list[int] = [], *, credential: Credential | None = None):
115
115
  """添加歌曲到歌单
116
116
 
@@ -128,7 +128,7 @@ async def add_songs(dirid: int = 1, song_ids: list[int] = [], *, credential: Cre
128
128
  }, lambda data: bool(data["result"]["updateTime"])
129
129
 
130
130
 
131
- @api_request("music.musicasset.PlaylistDetailWrite", "DelSonglist", verify=True, cacheable=False)
131
+ @api_request("music.musicasset.PlaylistDetailWrite", "DelSonglist", verify=True)
132
132
  async def del_songs(dirid: int = 1, song_ids: list[int] = [], *, credential: Credential | None = None):
133
133
  """删除歌单歌曲
134
134