qqmusic-api-python 0.3.4__tar.gz → 0.3.5__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 (43) hide show
  1. {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.5}/PKG-INFO +1 -1
  2. {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.5}/pyproject.toml +1 -1
  3. {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.5}/qqmusic_api/__init__.py +3 -2
  4. qqmusic_api_python-0.3.5/qqmusic_api/comment.py +91 -0
  5. {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.5}/qqmusic_api/login.py +10 -4
  6. {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.5}/qqmusic_api/utils/network.py +8 -9
  7. qqmusic_api_python-0.3.5/tests/test_comments.py +15 -0
  8. {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.5}/tests/test_song.py +1 -1
  9. {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.5}/.gitignore +0 -0
  10. {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.5}/LICENSE +0 -0
  11. {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.5}/README.md +0 -0
  12. {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.5}/qqmusic_api/album.py +0 -0
  13. {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.5}/qqmusic_api/exceptions/__init__.py +0 -0
  14. {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.5}/qqmusic_api/exceptions/api_exception.py +0 -0
  15. {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.5}/qqmusic_api/lyric.py +0 -0
  16. {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.5}/qqmusic_api/mv.py +0 -0
  17. {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.5}/qqmusic_api/search.py +0 -0
  18. {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.5}/qqmusic_api/singer.py +0 -0
  19. {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.5}/qqmusic_api/song.py +0 -0
  20. {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.5}/qqmusic_api/songlist.py +0 -0
  21. {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.5}/qqmusic_api/top.py +0 -0
  22. {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.5}/qqmusic_api/user.py +0 -0
  23. {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.5}/qqmusic_api/utils/__init__.py +0 -0
  24. {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.5}/qqmusic_api/utils/common.py +0 -0
  25. {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.5}/qqmusic_api/utils/credential.py +0 -0
  26. {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.5}/qqmusic_api/utils/device.py +0 -0
  27. {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.5}/qqmusic_api/utils/qimei.py +0 -0
  28. {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.5}/qqmusic_api/utils/session.py +0 -0
  29. {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.5}/qqmusic_api/utils/sign.py +0 -0
  30. {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.5}/qqmusic_api/utils/tripledes.py +0 -0
  31. {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.5}/tests/test_album.py +0 -0
  32. {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.5}/tests/test_login.py +0 -0
  33. {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.5}/tests/test_lyric.py +0 -0
  34. {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.5}/tests/test_mv.py +0 -0
  35. {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.5}/tests/test_qimei.py +0 -0
  36. {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.5}/tests/test_search.py +0 -0
  37. {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.5}/tests/test_session.py +0 -0
  38. {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.5}/tests/test_sign.py +0 -0
  39. {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.5}/tests/test_singer.py +0 -0
  40. {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.5}/tests/test_songlist.py +0 -0
  41. {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.5}/tests/test_top.py +0 -0
  42. {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.5}/tests/test_user.py +0 -0
  43. {qqmusic_api_python-0.3.4 → qqmusic_api_python-0.3.5}/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.4
3
+ Version: 0.3.5
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
@@ -60,7 +60,7 @@ include = ["/qqmusic_api", "/tests", "LISENCE", "README.md"]
60
60
  [dependency-groups]
61
61
  testing = [
62
62
  "pytest<9.0.0,>=8.2.0",
63
- "pytest-asyncio<1.0.0,>=0.23.6",
63
+ "pytest-asyncio<1.1.0,>=1.0.0",
64
64
  "pytest-sugar<2.0.0,>=1.0.0",
65
65
  ]
66
66
  docs = [
@@ -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.4"
7
+ __version__ = "0.3.5"
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,91 @@
1
+ """评论 API"""
2
+
3
+ from typing import Any
4
+
5
+ from qqmusic_api.utils.network import api_request
6
+
7
+
8
+ @api_request("music.globalComment.CommentRead", "GetHotCommentList")
9
+ async def get_hot_comments(
10
+ biz_id: str,
11
+ page_num: int = 1,
12
+ page_size: int = 15,
13
+ last_comment_seq_no: str = "",
14
+ ):
15
+ """获取歌曲热评
16
+
17
+ Args:
18
+ biz_id: 歌曲 ID
19
+ page_num: 页码
20
+ page_size: 每页数量
21
+ last_comment_seq_no: 上一页最后一条评论 ID(可选)
22
+ """
23
+ params = {
24
+ "BizType": 1,
25
+ "BizId": biz_id,
26
+ "LastCommentSeqNo": last_comment_seq_no,
27
+ "PageSize": page_size,
28
+ "PageNum": page_num,
29
+ "HotType": 1,
30
+ "WithAirborne": 0,
31
+ "PicEnable": 1,
32
+ }
33
+
34
+ def _processor(data: dict[str, Any]):
35
+ """处理并返回结构化评论数据:
36
+
37
+ 返回结构:
38
+ [
39
+ {
40
+ "Avatar": str, # 用户头像 URL
41
+ "CmId": str, # 评论 ID (后续需要获取全部子评论时需用到)
42
+ "PraiseNum": int, # 点赞数
43
+ "Nick": str, # 昵称
44
+ "Pic": str, # 评论配图 (可能为空)
45
+ "Content": str, # 评论内容
46
+ "SeqNo": str, # 评论序号 ID 可以用于传递给 参数: last_comment_seq_no
47
+ "SubComments": [ # 子评论列表
48
+ {
49
+ "Avatar": str,
50
+ "Nick": str,
51
+ "Content": str,
52
+ "Pic": str,
53
+ "PraiseNum": int,
54
+ "SeqNo": str
55
+ }
56
+ ]
57
+ },
58
+ ...
59
+ ]
60
+ """
61
+ comments = data.get("CommentList", {}).get("Comments", [])
62
+ result = []
63
+
64
+ for comment in comments:
65
+ item = {
66
+ "Avatar": comment.get("Avatar"),
67
+ "CmId": comment.get("CmId"),
68
+ "PraiseNum": comment.get("PraiseNum"),
69
+ "Nick": comment.get("Nick"),
70
+ "Pic": comment.get("Pic"),
71
+ "Content": comment.get("Content"),
72
+ "SeqNo": comment.get("SeqNo"),
73
+ "SubComments": [],
74
+ }
75
+
76
+ for sub in comment.get("SubComments", []):
77
+ sub_item = {
78
+ "Avatar": sub.get("Avatar"),
79
+ "Nick": sub.get("Nick"),
80
+ "Content": sub.get("Content"),
81
+ "Pic": sub.get("Pic"),
82
+ "PraiseNum": sub.get("PraiseNum"),
83
+ "SeqNo": sub.get("SeqNo"),
84
+ }
85
+ item["SubComments"].append(sub_item)
86
+
87
+ result.append(item)
88
+
89
+ return result
90
+
91
+ 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%3A%252F%252Fy.qq.com%252F",
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
- "music.login.LoginServer",
363
- "Login",
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:
@@ -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 commom(self, value: dict[str, Any]):
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) - 1,
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,15 @@
1
+ import pytest
2
+
3
+ from qqmusic_api.comment import 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"]
@@ -50,4 +50,4 @@ async def test_get_song_urls():
50
50
 
51
51
 
52
52
  async def test_get_fav_num():
53
- assert await song.get_fav_num(songid = [438910555, 9063002])
53
+ assert await song.get_fav_num(songid=[438910555, 9063002])