qqmusic-api-python 0.3.3__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.3 → qqmusic_api_python-0.3.5}/PKG-INFO +2 -6
  2. {qqmusic_api_python-0.3.3 → qqmusic_api_python-0.3.5}/README.md +1 -5
  3. {qqmusic_api_python-0.3.3 → qqmusic_api_python-0.3.5}/pyproject.toml +1 -1
  4. {qqmusic_api_python-0.3.3 → qqmusic_api_python-0.3.5}/qqmusic_api/__init__.py +3 -2
  5. qqmusic_api_python-0.3.5/qqmusic_api/comment.py +91 -0
  6. {qqmusic_api_python-0.3.3 → qqmusic_api_python-0.3.5}/qqmusic_api/login.py +10 -4
  7. {qqmusic_api_python-0.3.3 → qqmusic_api_python-0.3.5}/qqmusic_api/song.py +4 -1
  8. {qqmusic_api_python-0.3.3 → qqmusic_api_python-0.3.5}/qqmusic_api/songlist.py +4 -4
  9. {qqmusic_api_python-0.3.3 → qqmusic_api_python-0.3.5}/qqmusic_api/user.py +1 -1
  10. {qqmusic_api_python-0.3.3 → qqmusic_api_python-0.3.5}/qqmusic_api/utils/credential.py +2 -4
  11. {qqmusic_api_python-0.3.3 → qqmusic_api_python-0.3.5}/qqmusic_api/utils/network.py +8 -9
  12. qqmusic_api_python-0.3.5/tests/test_comments.py +15 -0
  13. {qqmusic_api_python-0.3.3 → qqmusic_api_python-0.3.5}/tests/test_song.py +1 -1
  14. qqmusic_api_python-0.3.5/web/README.md +46 -0
  15. {qqmusic_api_python-0.3.3 → qqmusic_api_python-0.3.5}/.gitignore +0 -0
  16. {qqmusic_api_python-0.3.3 → qqmusic_api_python-0.3.5}/LICENSE +0 -0
  17. {qqmusic_api_python-0.3.3 → qqmusic_api_python-0.3.5}/qqmusic_api/album.py +0 -0
  18. {qqmusic_api_python-0.3.3 → qqmusic_api_python-0.3.5}/qqmusic_api/exceptions/__init__.py +0 -0
  19. {qqmusic_api_python-0.3.3 → qqmusic_api_python-0.3.5}/qqmusic_api/exceptions/api_exception.py +0 -0
  20. {qqmusic_api_python-0.3.3 → qqmusic_api_python-0.3.5}/qqmusic_api/lyric.py +0 -0
  21. {qqmusic_api_python-0.3.3 → qqmusic_api_python-0.3.5}/qqmusic_api/mv.py +0 -0
  22. {qqmusic_api_python-0.3.3 → qqmusic_api_python-0.3.5}/qqmusic_api/search.py +0 -0
  23. {qqmusic_api_python-0.3.3 → qqmusic_api_python-0.3.5}/qqmusic_api/singer.py +0 -0
  24. {qqmusic_api_python-0.3.3 → qqmusic_api_python-0.3.5}/qqmusic_api/top.py +0 -0
  25. {qqmusic_api_python-0.3.3 → qqmusic_api_python-0.3.5}/qqmusic_api/utils/__init__.py +0 -0
  26. {qqmusic_api_python-0.3.3 → qqmusic_api_python-0.3.5}/qqmusic_api/utils/common.py +0 -0
  27. {qqmusic_api_python-0.3.3 → qqmusic_api_python-0.3.5}/qqmusic_api/utils/device.py +0 -0
  28. {qqmusic_api_python-0.3.3 → qqmusic_api_python-0.3.5}/qqmusic_api/utils/qimei.py +0 -0
  29. {qqmusic_api_python-0.3.3 → qqmusic_api_python-0.3.5}/qqmusic_api/utils/session.py +0 -0
  30. {qqmusic_api_python-0.3.3 → qqmusic_api_python-0.3.5}/qqmusic_api/utils/sign.py +0 -0
  31. {qqmusic_api_python-0.3.3 → qqmusic_api_python-0.3.5}/qqmusic_api/utils/tripledes.py +0 -0
  32. {qqmusic_api_python-0.3.3 → qqmusic_api_python-0.3.5}/tests/test_album.py +0 -0
  33. {qqmusic_api_python-0.3.3 → qqmusic_api_python-0.3.5}/tests/test_login.py +0 -0
  34. {qqmusic_api_python-0.3.3 → qqmusic_api_python-0.3.5}/tests/test_lyric.py +0 -0
  35. {qqmusic_api_python-0.3.3 → qqmusic_api_python-0.3.5}/tests/test_mv.py +0 -0
  36. {qqmusic_api_python-0.3.3 → qqmusic_api_python-0.3.5}/tests/test_qimei.py +0 -0
  37. {qqmusic_api_python-0.3.3 → qqmusic_api_python-0.3.5}/tests/test_search.py +0 -0
  38. {qqmusic_api_python-0.3.3 → qqmusic_api_python-0.3.5}/tests/test_session.py +0 -0
  39. {qqmusic_api_python-0.3.3 → qqmusic_api_python-0.3.5}/tests/test_sign.py +0 -0
  40. {qqmusic_api_python-0.3.3 → qqmusic_api_python-0.3.5}/tests/test_singer.py +0 -0
  41. {qqmusic_api_python-0.3.3 → qqmusic_api_python-0.3.5}/tests/test_songlist.py +0 -0
  42. {qqmusic_api_python-0.3.3 → qqmusic_api_python-0.3.5}/tests/test_top.py +0 -0
  43. {qqmusic_api_python-0.3.3 → qqmusic_api_python-0.3.5}/tests/test_user.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qqmusic-api-python
3
- Version: 0.3.3
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
@@ -98,11 +98,7 @@ async def main():
98
98
  asyncio.run(main())
99
99
  ```
100
100
 
101
- ## Web API
102
- ```
103
- docker build . -t qq-music-api
104
- docker run -d -p 8000:8000 qq-music-api
105
- ```
101
+ ## [Web API](./web/README.md)
106
102
 
107
103
  ## Licence
108
104
 
@@ -68,11 +68,7 @@ async def main():
68
68
  asyncio.run(main())
69
69
  ```
70
70
 
71
- ## Web API
72
- ```
73
- docker build . -t qq-music-api
74
- docker run -d -p 8000:8000 qq-music-api
75
- ```
71
+ ## [Web API](./web/README.md)
76
72
 
77
73
  ## Licence
78
74
 
@@ -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.3"
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:
@@ -16,7 +16,7 @@ def _get_extract_func(key: str):
16
16
 
17
17
 
18
18
  @api_request("music.trackInfo.UniformRuleCtrl", "CgiGetTrackInfo")
19
- async def query_song(value: list[str] | list[int]):
19
+ async def query_song(value: list[int] | list[str]):
20
20
  """根据 id 或 mid 获取歌曲信息
21
21
 
22
22
  Args:
@@ -114,6 +114,7 @@ class EncryptedSongFileType(BaseSongFileType):
114
114
  async def get_song_urls(
115
115
  mid: list[str],
116
116
  file_type: SongFileType = SongFileType.MP3_128,
117
+ *,
117
118
  credential: Credential | None = None,
118
119
  ) -> dict[str, str]: ...
119
120
 
@@ -122,6 +123,7 @@ async def get_song_urls(
122
123
  async def get_song_urls(
123
124
  mid: list[str],
124
125
  file_type: EncryptedSongFileType,
126
+ *,
125
127
  credential: Credential | None = None,
126
128
  ) -> dict[str, tuple[str, str]]: ...
127
129
 
@@ -129,6 +131,7 @@ async def get_song_urls(
129
131
  async def get_song_urls(
130
132
  mid: list[str],
131
133
  file_type: SongFileType | EncryptedSongFileType = SongFileType.MP3_128,
134
+ *,
132
135
  credential: Credential | None = None,
133
136
  ) -> dict[str, str] | dict[str, tuple[str, str]]:
134
137
  """获取歌曲文件链接
@@ -79,7 +79,7 @@ async def get_songlist(
79
79
 
80
80
 
81
81
  @api_request("music.musicasset.PlaylistBaseWrite", "AddPlaylist", verify=True, cacheable=False)
82
- async def create(dirname: str, credential: Credential | None = None):
82
+ async def create(dirname: str, *, credential: Credential | None = None):
83
83
  """添加歌单, 重名会在名称后面添加时间戳
84
84
 
85
85
  Args:
@@ -95,7 +95,7 @@ async def create(dirname: str, credential: Credential | None = None):
95
95
 
96
96
 
97
97
  @api_request("music.musicasset.PlaylistBaseWrite", "DelPlaylist", verify=True, cacheable=False)
98
- async def delete(dirid: int, credential: Credential | None = None):
98
+ async def delete(dirid: int, *, credential: Credential | None = None):
99
99
  """删除歌单
100
100
 
101
101
  Args:
@@ -111,7 +111,7 @@ async def delete(dirid: int, credential: Credential | None = None):
111
111
 
112
112
 
113
113
  @api_request("music.musicasset.PlaylistDetailWrite", "AddSonglist", verify=True, cacheable=False)
114
- async def add_songs(dirid: int = 1, song_ids: list[int] = [], credential: Credential | None = None):
114
+ async def add_songs(dirid: int = 1, song_ids: list[int] = [], *, credential: Credential | None = None):
115
115
  """添加歌曲到歌单
116
116
 
117
117
  Args:
@@ -129,7 +129,7 @@ async def add_songs(dirid: int = 1, song_ids: list[int] = [], credential: Creden
129
129
 
130
130
 
131
131
  @api_request("music.musicasset.PlaylistDetailWrite", "DelSonglist", verify=True, cacheable=False)
132
- async def del_songs(dirid: int = 1, song_ids: list[int] = [], credential: Credential | None = None):
132
+ async def del_songs(dirid: int = 1, song_ids: list[int] = [], *, credential: Credential | None = None):
133
133
  """删除歌单歌曲
134
134
 
135
135
  Args:
@@ -93,7 +93,7 @@ async def get_friend(page: int = 1, num: int = 10, *, credential: Credential | N
93
93
  return {
94
94
  "PageSize": num,
95
95
  "Page": page - 1,
96
- }, lambda data: {"total": data.get("Total", 0), "list": data.get("List", [])}
96
+ }, lambda data: {"total": len(data.get("Friends", [])), "list": data.get("Friends", [])}
97
97
 
98
98
 
99
99
  @api_request("music.concern.RelationList", "GetFollowUserList", verify=True, cacheable=False)
@@ -84,16 +84,14 @@ class Credential:
84
84
  if not self.has_musicid() or not self.has_musickey():
85
85
  return False
86
86
  if await self.is_expired():
87
- return bool(self.refresh_key) and bool(self.refresh_token)
87
+ return bool(self.refresh_key)
88
88
  return True
89
89
 
90
90
  async def is_expired(self) -> bool:
91
91
  """判断 credential 是否过期"""
92
92
  if "musickeyCreateTime" in self.extra_fields and "keyExpiresIn" in self.extra_fields:
93
93
  expired_time_stamp = self.extra_fields["musickeyCreateTime"] + self.extra_fields["keyExpiresIn"]
94
- if expired_time_stamp >= time():
95
- return True
96
- return False
94
+ return expired_time_stamp <= time()
97
95
 
98
96
  from ..login import check_expired
99
97
 
@@ -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])
@@ -0,0 +1,46 @@
1
+ # Web Port 使用说明
2
+
3
+ ## 1. 安装与运行
4
+
5
+ ### 克隆仓库
6
+
7
+ ```bash
8
+ git clone https://github.com/luren-dc/QQMusicApi
9
+ ```
10
+
11
+ ### 依赖安装
12
+
13
+ ```bash
14
+ uv sync --group web
15
+ ```
16
+
17
+ ### 启动服务
18
+
19
+ ```bash
20
+ uv run uvicorn web.app:app --host 0.0.0.0 --port 8000 --reload
21
+ ```
22
+
23
+ ### Docker
24
+
25
+ ```bash
26
+ docker build . -t qq-music-api
27
+ docker run -d -p 8000:8000 qq-music-api
28
+ ```
29
+
30
+ ## 2. API Endpoint
31
+
32
+ - **请求格式**: `GET /{module}/{func}`
33
+ - **示例**:
34
+ `GET /song/get_detail?id=12345`
35
+
36
+ ## 3. 请求参数规则
37
+
38
+ ### 类型转换规则
39
+
40
+ | 参数类型 | 示例值 | 说明 |
41
+ | ----------- | ------------------------------- | ------------------------------------ |
42
+ | `int` | `count=5` | 整数 |
43
+ | `bool` | `is_vip=true` | `true`/`1`/`yes` 或 `false`/`0`/`no` |
44
+ | `datetime` | `date=2023-10-01T12:34` | ISO 8601 格式 |
45
+ | `list[int]` | `id=1,2,3` | 逗号分隔的字符串 |
46
+ | `Enum` | `type=SongType.HIT`or`type=HIT` | 枚举名或值(见具体模块定义) |