qqmusic-api-python 0.1.0__tar.gz → 0.1.1__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 (33) hide show
  1. {qqmusic_api_python-0.1.0 → qqmusic_api_python-0.1.1}/PKG-INFO +14 -10
  2. {qqmusic_api_python-0.1.0 → qqmusic_api_python-0.1.1}/README.md +10 -6
  3. {qqmusic_api_python-0.1.0 → qqmusic_api_python-0.1.1}/pyproject.toml +15 -16
  4. qqmusic_api_python-0.1.1/qqmusic_api/__init__.py +14 -0
  5. {qqmusic_api_python-0.1.0 → qqmusic_api_python-0.1.1}/qqmusic_api/api/album.py +3 -0
  6. {qqmusic_api_python-0.1.0 → qqmusic_api_python-0.1.1}/qqmusic_api/api/login.py +9 -14
  7. {qqmusic_api_python-0.1.0 → qqmusic_api_python-0.1.1}/qqmusic_api/api/mv.py +3 -0
  8. qqmusic_api_python-0.1.1/qqmusic_api/api/singer.py +260 -0
  9. {qqmusic_api_python-0.1.0 → qqmusic_api_python-0.1.1}/qqmusic_api/api/song.py +75 -21
  10. {qqmusic_api_python-0.1.0 → qqmusic_api_python-0.1.1}/qqmusic_api/api/songlist.py +11 -3
  11. {qqmusic_api_python-0.1.0 → qqmusic_api_python-0.1.1}/qqmusic_api/api/top.py +13 -8
  12. qqmusic_api_python-0.1.1/qqmusic_api/data/api/singer.json +48 -0
  13. qqmusic_api_python-0.1.1/qqmusic_api/exceptions.py +106 -0
  14. {qqmusic_api_python-0.1.0 → qqmusic_api_python-0.1.1}/qqmusic_api/utils/credential.py +27 -26
  15. {qqmusic_api_python-0.1.0 → qqmusic_api_python-0.1.1}/qqmusic_api/utils/network.py +11 -12
  16. qqmusic_api_python-0.1.0/qqmusic_api/__init__.py +0 -4
  17. qqmusic_api_python-0.1.0/qqmusic_api/data/api/singer.json +0 -1
  18. qqmusic_api_python-0.1.0/qqmusic_api/exceptions.py +0 -121
  19. {qqmusic_api_python-0.1.0 → qqmusic_api_python-0.1.1}/LICENSE +0 -0
  20. {qqmusic_api_python-0.1.0 → qqmusic_api_python-0.1.1}/qqmusic_api/api/search.py +0 -0
  21. {qqmusic_api_python-0.1.0 → qqmusic_api_python-0.1.1}/qqmusic_api/data/api/album.json +0 -0
  22. {qqmusic_api_python-0.1.0 → qqmusic_api_python-0.1.1}/qqmusic_api/data/api/login.json +0 -0
  23. {qqmusic_api_python-0.1.0 → qqmusic_api_python-0.1.1}/qqmusic_api/data/api/mv.json +0 -0
  24. {qqmusic_api_python-0.1.0 → qqmusic_api_python-0.1.1}/qqmusic_api/data/api/search.json +0 -0
  25. {qqmusic_api_python-0.1.0 → qqmusic_api_python-0.1.1}/qqmusic_api/data/api/song.json +0 -0
  26. {qqmusic_api_python-0.1.0 → qqmusic_api_python-0.1.1}/qqmusic_api/data/api/songlist.json +0 -0
  27. {qqmusic_api_python-0.1.0 → qqmusic_api_python-0.1.1}/qqmusic_api/data/api/top.json +0 -0
  28. {qqmusic_api_python-0.1.0 → qqmusic_api_python-0.1.1}/qqmusic_api/data/file_type.json +0 -0
  29. {qqmusic_api_python-0.1.0 → qqmusic_api_python-0.1.1}/qqmusic_api/data/search_type.json +0 -0
  30. {qqmusic_api_python-0.1.0 → qqmusic_api_python-0.1.1}/qqmusic_api/settings.py +0 -0
  31. {qqmusic_api_python-0.1.0 → qqmusic_api_python-0.1.1}/qqmusic_api/utils/__init__.py +0 -0
  32. {qqmusic_api_python-0.1.0 → qqmusic_api_python-0.1.1}/qqmusic_api/utils/common.py +0 -0
  33. {qqmusic_api_python-0.1.0 → qqmusic_api_python-0.1.1}/qqmusic_api/utils/qimei.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: qqmusic-api-python
3
- Version: 0.1.0
3
+ Version: 0.1.1
4
4
  Summary: QQ音乐API封装库
5
5
  Home-page: https://github.com/luren-dc/QQMusicApi
6
6
  License: MIT
@@ -24,15 +24,15 @@ Classifier: Programming Language :: Python :: 3.12
24
24
  Classifier: Programming Language :: Python :: 3 :: Only
25
25
  Classifier: Programming Language :: Python :: Implementation :: CPython
26
26
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
27
- Requires-Dist: aiohttp (==3.9.5)
28
- Requires-Dist: cryptography (==41.0.2)
29
- Requires-Dist: requests (==2.31.0)
27
+ Requires-Dist: aiohttp (>=3.9.5,<4.0.0)
28
+ Requires-Dist: cryptography (>=41.0.2,<42.0.0)
29
+ Requires-Dist: requests (>=2.31.0,<3.0.0)
30
30
  Project-URL: Documentation, https://github.com/luren-dc/QQMusicApi
31
31
  Project-URL: Repository, https://github.com/luren-dc/QQMusicApi
32
32
  Description-Content-Type: text/markdown
33
33
 
34
34
  <div align="center">
35
- <h1> QQMusicApi </h1>
35
+ <h1> QQMusic Api </h1>
36
36
  <p> Python QQ音乐 API 封装库 </p>
37
37
 
38
38
  ![Python Version 3.9+](https://img.shields.io/badge/Python-3.9%2B-blue)
@@ -46,6 +46,8 @@ Description-Content-Type: text/markdown
46
46
  > [!WARNING]
47
47
  > 本仓库的所有内容仅供学习和参考之用,禁止用于商业用途。
48
48
 
49
+ 文档: [https://luren-dc.github.io/QQMusicApi/](https://luren-dc.github.io/QQMusicApi/)
50
+
49
51
  ## 介绍
50
52
 
51
53
  使用 Python 编写的用于调用 [QQ音乐](https://y.qq.com/) 各种 API 的库.
@@ -62,13 +64,13 @@ Description-Content-Type: text/markdown
62
64
 
63
65
  ### 安装
64
66
 
65
- ```
66
- $ pip3 install qqmusic-api-python
67
+ ```shell
68
+ $ pip install qqmusic-api-python
67
69
  ```
68
70
 
69
71
  ### 使用
70
72
 
71
- ```
73
+ ```python
72
74
  import asyncio
73
75
 
74
76
  from qqmusic_api import search
@@ -86,14 +88,16 @@ if __name__ == "__main__":
86
88
  ## TODO
87
89
 
88
90
  - [ ] 歌手 API
91
+ - [ ] 评论 API
92
+ - [ ] 用户 API
89
93
 
90
94
  ## 参考项目
91
95
 
92
- - [Rain120/qq-muisc-api](https://github.com/Rain120/qq-muisc-api)
96
+ - [Rain120/qq-muisc-api](https://github.com/Rain120/qq-music-api)
93
97
  - [jsososo/QQMusicApi](https://github.com/jsososo/QQMusicApi)
94
98
  - [Nemo2011/bilibili-api](https://github.com/Nemo2011/bilibili-api/)
95
99
 
96
100
  ## Licence
97
101
 
98
- **[MIT License](https://github.com/luren-dc/QQMusicApi/blob/master/LICENSE)**
102
+ **[MIT License](https://github.com/luren-dc/QQMusicApi?tab=MIT-1-ov-file)**
99
103
 
@@ -1,5 +1,5 @@
1
1
  <div align="center">
2
- <h1> QQMusicApi </h1>
2
+ <h1> QQMusic Api </h1>
3
3
  <p> Python QQ音乐 API 封装库 </p>
4
4
 
5
5
  ![Python Version 3.9+](https://img.shields.io/badge/Python-3.9%2B-blue)
@@ -13,6 +13,8 @@
13
13
  > [!WARNING]
14
14
  > 本仓库的所有内容仅供学习和参考之用,禁止用于商业用途。
15
15
 
16
+ 文档: [https://luren-dc.github.io/QQMusicApi/](https://luren-dc.github.io/QQMusicApi/)
17
+
16
18
  ## 介绍
17
19
 
18
20
  使用 Python 编写的用于调用 [QQ音乐](https://y.qq.com/) 各种 API 的库.
@@ -29,13 +31,13 @@
29
31
 
30
32
  ### 安装
31
33
 
32
- ```
33
- $ pip3 install qqmusic-api-python
34
+ ```shell
35
+ $ pip install qqmusic-api-python
34
36
  ```
35
37
 
36
38
  ### 使用
37
39
 
38
- ```
40
+ ```python
39
41
  import asyncio
40
42
 
41
43
  from qqmusic_api import search
@@ -53,13 +55,15 @@ if __name__ == "__main__":
53
55
  ## TODO
54
56
 
55
57
  - [ ] 歌手 API
58
+ - [ ] 评论 API
59
+ - [ ] 用户 API
56
60
 
57
61
  ## 参考项目
58
62
 
59
- - [Rain120/qq-muisc-api](https://github.com/Rain120/qq-muisc-api)
63
+ - [Rain120/qq-muisc-api](https://github.com/Rain120/qq-music-api)
60
64
  - [jsososo/QQMusicApi](https://github.com/jsososo/QQMusicApi)
61
65
  - [Nemo2011/bilibili-api](https://github.com/Nemo2011/bilibili-api/)
62
66
 
63
67
  ## Licence
64
68
 
65
- **[MIT License](https://github.com/luren-dc/QQMusicApi/blob/master/LICENSE)**
69
+ **[MIT License](https://github.com/luren-dc/QQMusicApi?tab=MIT-1-ov-file)**
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "qqmusic-api-python"
3
- version = "0.1.0"
3
+ version = "0.1.1"
4
4
  description = "QQ音乐API封装库"
5
5
  authors = ["Luren <dluren.c@gmail.com>"]
6
6
  packages = [{ include = "qqmusic_api" }]
@@ -26,18 +26,22 @@ classifiers = [
26
26
 
27
27
  [tool.poetry.dependencies]
28
28
  python = "^3.9"
29
- cryptography = "41.0.2"
30
- requests = "2.31.0"
31
- aiohttp = "3.9.5"
29
+ cryptography = "^41.0.2"
30
+ requests = "^2.31.0"
31
+ aiohttp = "^3.9.5"
32
32
 
33
33
  [tool.poetry.group.dev.dependencies]
34
- pytest = "8.2.0"
35
- pytest-asyncio = "0.23.6"
36
- qrcode = { extras = ["pil"], version = "7.4.2" }
37
- pyzbar = "0.1.9"
38
- pytest-timeout = "2.3.1"
39
- pytest-sugar = "1.0.0"
40
- pre-commit = "3.7.0"
34
+ pytest = "^8.2.0"
35
+ pytest-asyncio = "^0.23.6"
36
+ qrcode = { extras = ["pil"], version = "^7.4.2" }
37
+ pyzbar = "^0.1.9"
38
+ pytest-timeout = "^2.3.1"
39
+ pytest-sugar = "^1.0.0"
40
+ pre-commit = "^3.7.0"
41
+
42
+ [build-system]
43
+ requires = ["poetry-core"]
44
+ build-backend = "poetry.core.masonry.api"
41
45
 
42
46
  [tool.mypy]
43
47
  disable_error_code = ["index", "arg-type", "union-attr", "return-value"]
@@ -48,8 +52,3 @@ show_column_numbers = true
48
52
  pythonpath = "./"
49
53
  timeout = 30
50
54
  testpaths = ["tests"]
51
-
52
-
53
- [build-system]
54
- requires = ["poetry-core"]
55
- build-backend = "poetry.core.masonry.api"
@@ -0,0 +1,14 @@
1
+ from .api import album, mv, login, search, song, songlist, top, singer
2
+ from .utils.credential import Credential
3
+
4
+ __all__ = [
5
+ "album",
6
+ "Credential",
7
+ "login",
8
+ "mv",
9
+ "search",
10
+ "singer",
11
+ "song",
12
+ "songlist",
13
+ "top",
14
+ ]
@@ -18,6 +18,9 @@ class Album:
18
18
  """
19
19
  self.mid = mid
20
20
 
21
+ def __repr__(self) -> str:
22
+ return f"Album(mid={self.mid})"
23
+
21
24
  async def get_detail(self) -> dict:
22
25
  """
23
26
  Returns:
@@ -5,7 +5,6 @@ from abc import ABC, abstractmethod
5
5
  from enum import Enum
6
6
  from typing import Optional
7
7
 
8
- from ..exceptions import AuthcodeExpiredException, ResponseCodeException
9
8
  from ..utils.common import get_api, hash33, random_uuid
10
9
  from ..utils.credential import Credential
11
10
  from ..utils.network import Api, get_aiohttp_session
@@ -15,7 +14,7 @@ API = get_api("login")
15
14
 
16
15
  class QrCodeLoginEvents(Enum):
17
16
  """
18
- 二维码登录状态枚举
17
+ 二维码登录状态
19
18
 
20
19
  + SCAN: 未扫描二维码
21
20
  + CONF: 未确认登录
@@ -35,7 +34,7 @@ class QrCodeLoginEvents(Enum):
35
34
 
36
35
  class PhoneLoginEvents(Enum):
37
36
  """
38
- 手机登录状态枚举
37
+ 手机登录状态
39
38
 
40
39
  + SEND: 发送成功
41
40
  + CAPTCHA: 需要滑块验证
@@ -400,17 +399,13 @@ class PhoneLogin(Login):
400
399
  if not authcode:
401
400
  raise ValueError("authcode 为空")
402
401
  params = {"code": str(authcode), "phoneNo": str(self.phone), "loginMode": 1}
403
- try:
404
- res = (
405
- await Api(**API["phone_login"])
406
- .update_params(**params)
407
- .update_extra_common(tmeLoginMethod="3")
408
- .result
409
- )
410
- return Credential.from_cookies(res)
411
- except ResponseCodeException as e:
412
- if e.code == 20271:
413
- raise AuthcodeExpiredException()
402
+ res = (
403
+ await Api(**API["phone_login"])
404
+ .update_params(**params)
405
+ .update_extra_common(tmeLoginMethod="3")
406
+ .result
407
+ )
408
+ return Credential.from_cookies(res)
414
409
 
415
410
 
416
411
  async def refresh_cookies(credential: Credential) -> Credential:
@@ -15,6 +15,9 @@ class MV:
15
15
  def __init__(self, vid: str):
16
16
  self.vid = vid
17
17
 
18
+ def __repr__(self) -> str:
19
+ return f"MV(vid={self.vid})"
20
+
18
21
  async def get_detail(self) -> dict:
19
22
  """
20
23
  获取 MV 详细信息
@@ -0,0 +1,260 @@
1
+ from enum import Enum
2
+ from typing import Optional
3
+
4
+ from ..utils.common import get_api
5
+ from ..utils.network import Api
6
+ from .song import Song
7
+
8
+ API = get_api("singer")
9
+
10
+
11
+ class AreaType(Enum):
12
+ """
13
+ 地区
14
+
15
+ + ALL: 全部
16
+ + CHINA: 内地
17
+ + TAIWAN: 台湾
18
+ + AMERICA: 美国
19
+ + EUROPE: 欧美
20
+ + JAPAN: 日本
21
+ + KOREA: 韩国
22
+ """
23
+
24
+ ALL = -100
25
+ CHINA = 200
26
+ TAIWAN = 2
27
+ AMERICA = 5
28
+ EUROPE = 4
29
+ JAPAN = 3
30
+ KOREA = 1
31
+
32
+
33
+ class GenreType(Enum):
34
+ """
35
+ 风格
36
+
37
+ + ALL: 全部
38
+ + POP: 流行
39
+ + RAP: 说唱
40
+ + CHINESE_STYLE: 国风
41
+ + ROCK: 摇滚
42
+ + ELECTRONIC: 电子
43
+ + FOLK: 民谣
44
+ + R_AND_B: R&B
45
+ + ETHNIC: 民族乐
46
+ + LIGHT_MUSIC: 轻音乐
47
+ + JAZZ: 爵士
48
+ + CLASSICAL: 古典
49
+ + COUNTRY: 乡村
50
+ + BLUES: 蓝调
51
+ """
52
+
53
+ ALL = -100
54
+ POP = 7
55
+ RAP = 3
56
+ CHINESE_STYLE = 19
57
+ ROCK = 4
58
+ ELECTRONIC = 2
59
+ FOLK = 8
60
+ R_AND_B = 11
61
+ ETHNIC = 37
62
+ LIGHT_MUSIC = 93
63
+ JAZZ = 14
64
+ CLASSICAL = 33
65
+ COUNTRY = 13
66
+ BLUES = 10
67
+
68
+
69
+ class SexType(Enum):
70
+ """
71
+ 性别
72
+
73
+ + ALL: 全部
74
+ + MALE: 男
75
+ + FEMALE: 女
76
+ + GROUP: 组合
77
+ """
78
+
79
+ ALL = -100
80
+ MALE = 0
81
+ FEMALE = 1
82
+ GROUP = 2
83
+
84
+
85
+ class TabType(Enum):
86
+ """
87
+ Tab 类型
88
+
89
+ + WIKI: wiki
90
+ + ALBUM: 专辑
91
+ + COMPOSER: 作曲
92
+ + LYRICIST: 作词
93
+ + PRODUCER: 制作人
94
+ + ARRANGER: 编曲
95
+ + MUSICIAN: 乐手
96
+ + SONG: 歌曲
97
+ + VIDEO: 视频
98
+ """
99
+
100
+ WIKI = ("wiki", "IntroductionTab")
101
+ ALBUM = ("album", "AlbumTab")
102
+ COMPOSER = ("song_composing", "SongTab")
103
+ LYRICIST = ("song_lyric", "SongTab")
104
+ PRODUCER = ("producer", "SongTab")
105
+ ARRANGER = ("arranger", "SongTab")
106
+ MUSICIAN = ("musician", "SongTab")
107
+ SONG = ("song_sing", "SongTab")
108
+ VIDEO = ("video", "VideoTab")
109
+
110
+ def __init__(self, tabID: str, tabName: str) -> None:
111
+ super().__init__()
112
+ self.tabID = tabID
113
+ self.tabName = tabName
114
+
115
+
116
+ async def get_singer_list(
117
+ area: AreaType = AreaType.ALL,
118
+ sex: SexType = SexType.ALL,
119
+ genre: GenreType = GenreType.ALL,
120
+ ) -> list:
121
+ """
122
+ 获取歌手列表
123
+
124
+ Args:
125
+ area: 地区.Defaluts to AreaType.ALL
126
+ sex: 性别.Defaluts to SexType.ALL
127
+ genre: 风格.Defaluts to GenreType.ALL
128
+
129
+ Returns:
130
+ list: 歌手列表
131
+ """
132
+ result = (
133
+ await Api(**API["singer_list"])
134
+ .update_params(
135
+ hastag=0,
136
+ area=area.value,
137
+ sex=sex.value,
138
+ genre=genre.value,
139
+ )
140
+ .result
141
+ )
142
+ return result["hotlist"]
143
+
144
+
145
+ class Singer:
146
+ """
147
+ 歌手类
148
+ """
149
+
150
+ def __init__(self, mid: str) -> None:
151
+ """
152
+ Args:
153
+ mid: 歌手 mid
154
+ """
155
+ self.mid = mid
156
+ self._info: Optional[dict] = None
157
+
158
+ def __repr__(self) -> str:
159
+ return f"Singer(mid={self.mid})"
160
+
161
+ def __str__(self) -> str:
162
+ if self._info:
163
+ return str(self._info)
164
+ return f"Singer(mid={self.mid})"
165
+
166
+ async def __get_info(self) -> dict:
167
+ """
168
+ 获取歌手必要信息
169
+ """
170
+ if not self._info:
171
+ info = (
172
+ await Api(**API["homepage"]).update_params(SingerMid=self.mid).result
173
+ )["Info"]
174
+ self._info = {
175
+ "FansNum": info["FansNum"]["Num"],
176
+ }
177
+ self._info.update(info["Singer"])
178
+ return self._info
179
+
180
+ async def get_info(self) -> dict:
181
+ """
182
+ 获取歌手信息
183
+
184
+ Returns:
185
+ dict: 歌手信息
186
+ """
187
+ if not self._info:
188
+ self._info = await self.__get_info()
189
+ return self._info
190
+
191
+ async def get_fans_num(self) -> int:
192
+ """
193
+ 获取歌手粉丝数
194
+
195
+ Returns:
196
+ int: 粉丝数
197
+ """
198
+ return (await self.__get_info())["FansNum"]
199
+
200
+ async def get_tab_detail(self, tab_type: TabType, page: int = 1, num: int = 100):
201
+ """
202
+ 获取歌手 Tab 详细信息
203
+
204
+ Args:
205
+ tab_type: Tab 类型
206
+ page: 页码
207
+ num: 返回数量
208
+
209
+ Returns:
210
+ list: Tab 详细信息
211
+ """
212
+ return (
213
+ await Api(**API["homepage_tab_detail"])
214
+ .update_params(
215
+ SingerMid=self.mid,
216
+ IsQueryTabDetail=1,
217
+ TabID=tab_type.tabID,
218
+ PageNum=page - 1,
219
+ PageSize=num,
220
+ Order=0,
221
+ )
222
+ .result
223
+ )[tab_type.tabName]
224
+
225
+ async def get_wiki(self) -> dict:
226
+ """
227
+ 获取歌手WiKi
228
+
229
+ Returns:
230
+ dict: 歌手WiKi
231
+ """
232
+ return await self.get_tab_detail(TabType.WIKI)
233
+
234
+ async def get_song(
235
+ self, t: TabType = TabType.SONG, page: int = 1, num: int = 100
236
+ ) -> list[Song]:
237
+ """
238
+ 获取歌手歌曲
239
+
240
+ Args:
241
+ t: Tab 类型. Defaluts to TabType.SONG
242
+ page: 页码. Defaluts to 1
243
+ num: 返回数量. Defaluts to 100
244
+
245
+ Returns:
246
+ list: `Song` 列表
247
+ """
248
+ if t not in [
249
+ TabType.SONG,
250
+ TabType.COMPOSER,
251
+ TabType.LYRICIST,
252
+ TabType.PRODUCER,
253
+ TabType.MUSICIAN,
254
+ TabType.ARRANGER,
255
+ ]:
256
+ raise ValueError(
257
+ "t must be in [TabType.SONG, TabType.COMPOSER, TabType.LYRICIST, TabType.PRODUCER, TabType.MUSICIAN, TabType.ARRANGER]"
258
+ )
259
+ data = await self.get_tab_detail(t, page, num)
260
+ return Song.from_list(data["List"])
@@ -1,5 +1,10 @@
1
+ import asyncio
1
2
  from enum import Enum
2
- from typing import Optional
3
+ from typing import TYPE_CHECKING, Optional
4
+
5
+ if TYPE_CHECKING:
6
+ from .album import Album
7
+ from .singer import Singer
3
8
 
4
9
  from ..exceptions import ArgsException
5
10
  from ..utils.common import get_api, parse_song_info, random_string
@@ -73,20 +78,17 @@ class Song:
73
78
  self,
74
79
  mid: Optional[str] = None,
75
80
  id: Optional[int] = None,
76
- credential: Optional[Credential] = None,
77
81
  ):
78
82
  """
79
83
  Args:
80
84
  mid: 歌曲 mid. 歌曲 id 和歌曲 mid 必须提供其中之一
81
85
  id: 歌曲 id. 歌曲 id 和歌曲 mid 必须提供其中之一
82
- credential: Credential 类. Defaluts to None
83
86
  """
84
87
  # ID 检查
85
88
  if mid is None and id is None:
86
89
  raise ArgsException("请至少提供 mid 和 id 中的其中一个参数。")
87
90
  self._mid = mid
88
91
  self._id = id
89
- self.credential = Credential() if credential is None else credential
90
92
  self._info: Optional[dict] = None
91
93
 
92
94
  @classmethod
@@ -105,7 +107,20 @@ class Song:
105
107
  s._info = info
106
108
  return s
107
109
 
108
- async def __get_info(self):
110
+ @classmethod
111
+ def from_list(cls, data: list[dict]) -> list["Song"]:
112
+ """
113
+ 从列表新建 Song
114
+
115
+ Args:
116
+ data: 歌曲列表
117
+
118
+ Returns:
119
+ list: 歌曲列表
120
+ """
121
+ return [cls.from_dict(info) for info in data]
122
+
123
+ async def __get_info(self) -> dict:
109
124
  """
110
125
  获取歌曲必要信息
111
126
  """
@@ -114,7 +129,7 @@ class Song:
114
129
  self._info = (await query_by_mid([self._mid]))[0]
115
130
  elif self._id:
116
131
  self._info = (await query_by_id([self._id]))[0]
117
- return self._info
132
+ return self._info # type: ignore
118
133
 
119
134
  @property
120
135
  async def mid(self) -> str:
@@ -140,6 +155,14 @@ class Song:
140
155
  self._id = (await self.__get_info())["info"]["id"]
141
156
  return int(self._id)
142
157
 
158
+ def __repr__(self) -> str:
159
+ return f"Song(mid={self._mid}, id={self._id})"
160
+
161
+ def __str__(self) -> str:
162
+ if self._info:
163
+ return str(self._info)
164
+ return self.__repr__()
165
+
143
166
  async def __prepare_param(self, is_mid: bool = False, is_id: bool = False) -> dict:
144
167
  """
145
168
  准备请求参数
@@ -173,6 +196,28 @@ class Song:
173
196
  """
174
197
  return (await self.__get_info())["info"]
175
198
 
199
+ async def get_singer(self) -> "Singer":
200
+ """
201
+ 获取歌曲歌手
202
+
203
+ Returns:
204
+ Singer: 歌手
205
+ """
206
+ from .singer import Singer
207
+
208
+ return Singer((await self.__get_info())["singer"]["mid"])
209
+
210
+ async def get_album(self) -> "Album":
211
+ """
212
+ 获取歌曲专辑
213
+
214
+ Returns:
215
+ Album: 专辑
216
+ """
217
+ from .album import Album
218
+
219
+ return Album((await self.__get_info())["album"]["mid"])
220
+
176
221
  async def get_detail(self) -> dict:
177
222
  """
178
223
  获取歌曲详细信息
@@ -208,7 +253,7 @@ class Song:
208
253
  param = await self.__prepare_param(is_id=True)
209
254
  return (await Api(**API["labels"]).update_params(**param).result)["labels"]
210
255
 
211
- async def get_related_playlist(self) -> list[dict]:
256
+ async def get_related_songlist(self) -> list[dict]:
212
257
  """
213
258
  获取歌曲相关歌单
214
259
 
@@ -266,6 +311,7 @@ class Song:
266
311
  self,
267
312
  file_type: SongFileType = SongFileType.MP3_128,
268
313
  url_type: UrlType = UrlType.PLAY,
314
+ credential: Optional[Credential] = None,
269
315
  ) -> dict[str, str]:
270
316
  """
271
317
  获取歌曲文件链接
@@ -273,11 +319,12 @@ class Song:
273
319
  Args:
274
320
  file_type: 歌曲文件类型. Defaults to SongFileType.MP3_128
275
321
  url_type: 歌曲链接类型. Defaults to UrlType.PLAY
322
+ credential: 账号凭证. Defaults to None
276
323
 
277
324
  Returns:
278
325
  dict: 链接字典
279
326
  """
280
- return await get_song_urls([await self.mid], file_type, url_type)
327
+ return await get_song_urls([await self.mid], file_type, url_type, credential)
281
328
 
282
329
  async def get_file_size(self, file_type: Optional[SongFileType] = None) -> dict:
283
330
  """
@@ -372,18 +419,25 @@ async def get_song_urls(
372
419
  )
373
420
  api = Api(**API[url_type.value], credential=credential)
374
421
  urls = {}
422
+ tasks = []
375
423
  for mid in mid_list:
376
- # 构造请求参数
377
- file_name = [f"{file_type.s}{_}{_}{file_type.e}" for _ in mid]
378
- param = {
379
- "filename": file_name,
380
- "guid": random_string(32, "abcdef1234567890"),
381
- "songmid": mid,
382
- "songtype": [1 for _ in range(len(mid))],
383
- }
384
- res = await api.update_params(**param).result
385
- data = res["midurlinfo"]
386
- for info in data:
387
- song_url = domain + info["wifiurl"] if info["wifiurl"] else ""
388
- urls[info["songmid"]] = song_url
424
+
425
+ async def get_song_url(mid):
426
+ # 构造请求参数
427
+ file_name = [f"{file_type.s}{_}{_}{file_type.e}" for _ in mid]
428
+ param = {
429
+ "filename": file_name,
430
+ "guid": random_string(32, "abcdef1234567890"),
431
+ "songmid": mid,
432
+ "songtype": [1 for _ in range(len(mid))],
433
+ }
434
+
435
+ res = await api.update_params(**param).result
436
+ data = res["midurlinfo"]
437
+ for info in data:
438
+ song_url = domain + info["wifiurl"] if info["wifiurl"] else ""
439
+ urls[info["songmid"]] = song_url
440
+
441
+ tasks.append(asyncio.create_task(get_song_url(mid)))
442
+ await asyncio.gather(*tasks)
389
443
  return urls
@@ -1,9 +1,8 @@
1
1
  from typing import Optional
2
2
 
3
- from qqmusic_api.api.song import Song
4
-
5
3
  from ..utils.common import get_api
6
4
  from ..utils.network import Api
5
+ from .song import Song
7
6
 
8
7
  API = get_api("songlist")
9
8
 
@@ -21,6 +20,14 @@ class Songlist:
21
20
  self.id = id
22
21
  self._info: Optional[dict] = None
23
22
 
23
+ def __repr__(self) -> str:
24
+ return f"Songlist(id={self.id})"
25
+
26
+ def __str__(self) -> str:
27
+ if self._info:
28
+ return str(self._info)
29
+ return self.__repr__()
30
+
24
31
  async def __get_info(self):
25
32
  if not self._info:
26
33
  param = {
@@ -63,6 +70,7 @@ class Songlist:
63
70
  async def get_song_tag(self) -> list[dict]:
64
71
  """
65
72
  获取歌单歌曲标签
73
+ 注:存在几率返回为空
66
74
 
67
75
  Returns:
68
76
  list: 歌单歌曲标签
@@ -70,7 +78,7 @@ class Songlist:
70
78
  result = await self.__get_info()
71
79
  return result["songtag"]
72
80
 
73
- async def get_song_mid(self) -> list[dict]:
81
+ async def get_song_mid(self) -> list[str]:
74
82
  """
75
83
  获取歌单歌曲全部 mid
76
84
 
@@ -1,7 +1,9 @@
1
+ import datetime
2
+
1
3
  from qqmusic_api.api.song import Song
2
4
  from qqmusic_api.utils.network import Api
5
+
3
6
  from ..utils.common import get_api
4
- import datetime
5
7
 
6
8
  API = get_api("top")
7
9
 
@@ -9,10 +11,8 @@ API = get_api("top")
9
11
  async def get_top_category(show_detail: bool = False) -> list[dict]:
10
12
  """
11
13
  获取所有排行榜
12
-
13
14
  Args:
14
15
  show_detail: 是否显示详情(包括介绍,前三歌曲). Defaults to False
15
-
16
16
  Returns:
17
17
  list: 排行榜信息
18
18
  """
@@ -46,18 +46,23 @@ class Top:
46
46
  """
47
47
  Args:
48
48
  id: 排行榜 ID
49
- period: 排行榜周期
49
+ period: 排行榜时间
50
50
  """
51
51
  self.id = id
52
52
  self.set_period(period)
53
53
 
54
+ def __repr__(self) -> str:
55
+ return f"Top(id={self.id}, period={self.period})"
56
+
57
+ def __str__(self) -> str:
58
+ return self.__repr__()
59
+
54
60
  def set_period(self, period: str):
55
61
  """
56
- 设置排行榜周期
62
+ 设置排行榜时间
57
63
 
58
64
  Args:
59
- year:
60
- week: 周
65
+ period: 排行榜周期
61
66
  """
62
67
  time_type = "%Y-%m-%d" if self.id in [4, 27, 62] else "'%Y_%W"
63
68
  self.period = period or datetime.datetime.strftime(
@@ -95,4 +100,4 @@ class Top:
95
100
  """
96
101
  param = {"topId": self.id, "period": self.period, "offset": 0, "num": 100}
97
102
  result = await Api(**API["detail"]).update_params(**param).result
98
- return [Song.from_dict(song) for song in result["songInfoList"]]
103
+ return [Song(id=song["songId"]) for song in result["data"]["song"]]
@@ -0,0 +1,48 @@
1
+ {
2
+ "singer_list": {
3
+ "module": "music.musichallSinger.SingerList",
4
+ "method": "GetSingerList",
5
+ "params": {
6
+ "area": "int 地区码",
7
+ "sex": "int 性别",
8
+ "genre": "int 风格",
9
+ "hastag": "int 返回标签"
10
+ },
11
+ "comment": "获取歌手列表"
12
+ },
13
+ "homepage": {
14
+ "module": "music.UnifiedHomepage.UnifiedHomepageSrv",
15
+ "method": "GetHomepageHeader",
16
+ "params": {
17
+ "SingerMid": "str 歌手 mid",
18
+ "IsQueryTabDetail": "int 是否返回 Tab 详细信息",
19
+ "TabID": "int TabID",
20
+ "Order": "int 排序"
21
+ },
22
+ "comment": "获取歌手主页信息"
23
+ },
24
+ "desc": {
25
+ "module": "music.musichallSinger.SingerInfoInter",
26
+ "method": "GetSingerDetail",
27
+ "params": {
28
+ "singer_mids": "list 歌手 mid 列表",
29
+ "group_singer": "int 1",
30
+ "wiki_singer": "int 1"
31
+ },
32
+ "comment": "获取歌手简介"
33
+ },
34
+ "homepage_tab_detail": {
35
+ "module": "music.UnifiedHomepage.UnifiedHomepageSrv",
36
+ "method": "GetHomepageTabDetail",
37
+ "params": {
38
+ "SingerMid": "str 歌手 mid",
39
+ "IsQueryTabDetail": "int 是否返回 Tab 详细信息",
40
+ "TabID": "int TabID",
41
+ "PageNum": "int 页码",
42
+ "PageSize": "int 返回数量",
43
+ "StartIndex": "int 开始位置",
44
+ "Order": "int 排序"
45
+ },
46
+ "comment": "获取歌手首页 Tab 栏信息"
47
+ }
48
+ }
@@ -0,0 +1,106 @@
1
+ class ApiException(Exception):
2
+ """
3
+ API 异常基类
4
+ """
5
+
6
+ def __init__(self, msg: str = "未知原因"):
7
+ super().__init__(msg)
8
+ self.msg = msg
9
+
10
+ def __str__(self):
11
+ return self.msg
12
+
13
+
14
+ class ArgsException(ApiException):
15
+ """
16
+ 参数错误异常
17
+ """
18
+
19
+ pass
20
+
21
+
22
+ class ClientException(ApiException):
23
+ """
24
+ 服务器连接错误异常
25
+ """
26
+
27
+ def __init__(self):
28
+ super().__init__("连接到服务器时出现了问题")
29
+
30
+
31
+ class NetworkException(ApiException):
32
+ """
33
+ 网络错误异常
34
+ """
35
+
36
+ def __init__(self, status: int, msg: str):
37
+ full_msg = f"网络错误,状态码:{status} - {msg}"
38
+ super().__init__(full_msg)
39
+ self.status = status
40
+
41
+
42
+ class ResponseException(ApiException):
43
+ """
44
+ API 错误异常
45
+ """
46
+
47
+ def __init__(self, api: list):
48
+ super().__init__()
49
+ self.api = api
50
+
51
+ def __str__(self):
52
+ return f"接口信息:{'.'.join(self.api)}"
53
+
54
+
55
+ class CredentialCanNotRefreshException(ApiException):
56
+ """
57
+ Crediential 无法刷新异常
58
+ """
59
+
60
+ def __init__(self):
61
+ super().__init__("Crediential 无法刷新,请检查是否缺少必要字段")
62
+
63
+
64
+ class CredentialNoMusicidException(ApiException):
65
+ """
66
+ Crediential 缺少 Musicid 异常
67
+ """
68
+
69
+ def __init__(self):
70
+ super().__init__("Crediential 缺少 Musicid")
71
+
72
+
73
+ class CredentialNoMusickeyException(ApiException):
74
+ """
75
+ Crediential 缺少 Musickey 异常
76
+ """
77
+
78
+ def __init__(self):
79
+ super().__init__("Crediential 缺少 Musickey")
80
+
81
+
82
+ class LoginDevicesFullException(ApiException):
83
+ """
84
+ 登录设备已满异常
85
+ """
86
+
87
+ def __init__(self):
88
+ super().__init__("登录设备已满")
89
+
90
+
91
+ class AuthcodeExpiredException(ApiException):
92
+ """
93
+ 验证码已失效异常
94
+ """
95
+
96
+ def __init__(self):
97
+ super().__init__("验证码已失效")
98
+
99
+
100
+ class AuthcodeGetFrequentlyException(ApiException):
101
+ """
102
+ 验证码获取频繁异常
103
+ """
104
+
105
+ def __init__(self):
106
+ super().__init__("验证码获取频繁")
@@ -1,28 +1,29 @@
1
+ from dataclasses import asdict, dataclass, field
2
+ from typing import Any, Dict
3
+
1
4
  from ..exceptions import (
5
+ CredentialCanNotRefreshException,
2
6
  CredentialNoMusicidException,
3
7
  CredentialNoMusickeyException,
4
- CredientialCanNotRefreshException,
5
8
  )
6
9
 
7
10
 
11
+ @dataclass
8
12
  class Credential:
9
- def __init__(
10
- self, musicid: str = "", musickey: str = "", refresh_key: str = "", **kwagrs
11
- ):
12
- self.musicid = musicid
13
- self.musickey = musickey
14
- self.refresh_key = refresh_key
15
- self.login_type = 1 if "W_X" in musickey else 2
13
+ musicid: str = ""
14
+ musickey: str = ""
15
+ refresh_key: str = ""
16
+ login_type: int = field(init=False)
17
+ extra_fields: Dict[str, Any] = field(default_factory=dict)
16
18
 
17
- for key, value in kwagrs.items():
18
- setattr(self, key, value)
19
+ def __post_init__(self):
20
+ self.login_type = 1 if "W_X" in self.musickey else 2
19
21
 
20
22
  def get_dict(self) -> dict:
21
- cookies = {}
22
- for key, value in self.__dict__.items():
23
- if key not in cookies and value is not None:
24
- cookies[key] = value
25
- return cookies
23
+ """
24
+ 返回 Credential 的字典表示,包括所有字段。
25
+ """
26
+ return {**asdict(self), **self.extra_fields}
26
27
 
27
28
  def has_musicid(self) -> bool:
28
29
  """
@@ -44,10 +45,10 @@ class Credential:
44
45
 
45
46
  def raise_for_cannot_refresh(self):
46
47
  """
47
- 无法刷新 Credential 时则抛出异常
48
+ 无法刷新 Credential 时抛出异常
48
49
  """
49
50
  if not self.can_refresh():
50
- raise CredientialCanNotRefreshException()
51
+ raise CredentialCanNotRefreshException()
51
52
 
52
53
  def raise_for_no_musicid(self):
53
54
  """
@@ -75,18 +76,18 @@ class Credential:
75
76
  self.refresh_key = c.refresh_key
76
77
 
77
78
  @classmethod
78
- def from_cookies(cls, cookies: dict = {}) -> "Credential":
79
+ def from_cookies(cls, cookies: dict) -> "Credential":
79
80
  """
80
- 从 cookies 新建 Credential
81
+ 从 cookies 创建 Credential 实例
81
82
 
82
83
  Args:
83
- cookies : Cookies.
84
+ cookies : Cookies 字典.
84
85
 
85
86
  Returns:
86
- Credential: 凭据类
87
+ Credential: 凭据类实例
87
88
  """
88
- c = cls()
89
- c.musicid = cookies["musicid"]
90
- c.musickey = cookies["musickey"]
91
- c.refresh_key = cookies["refresh_key"]
92
- return c
89
+ return cls(
90
+ musicid=cookies.get("musicid", ""),
91
+ musickey=cookies.get("musickey", ""),
92
+ refresh_key=cookies.get("refresh_key", ""),
93
+ )
@@ -6,8 +6,9 @@ from typing import Any
6
6
 
7
7
  import aiohttp
8
8
 
9
+
9
10
  from .. import settings
10
- from ..exceptions import ClientException, NetworkException
11
+ from ..exceptions import ClientException, NetworkException, ResponseException
11
12
  from .credential import Credential
12
13
 
13
14
  HEADERS = {
@@ -78,28 +79,28 @@ class Api:
78
79
  self.params = {k: "" for k in self.params.keys()}
79
80
  self.headers = {k: "" for k in self.headers.keys()}
80
81
  self.extra_common = {k: "" for k in self.extra_common.keys()}
81
- self.__result: str | dict | None = None
82
+ self.__result: dict | None = None
82
83
 
83
84
  def __setattr__(self, __name: str, __value: Any) -> None:
84
85
  """
85
86
  每次更新参数都要把 __result 清除
86
87
  """
87
- if self.initialized and __name != "_Api__result":
88
+ if self.initialized and __name != "__Api__result":
88
89
  self.__result = None
89
90
  return super().__setattr__(__name, __value)
90
91
 
91
92
  @property
92
93
  def initialized(self):
93
- return "_Api__result" in self.__dict__
94
+ return "__Api__result" in self.__dict__
94
95
 
95
96
  @property
96
- async def result(self) -> dict | None:
97
+ async def result(self) -> dict:
97
98
  """
98
99
  获取请求结果
99
100
  """
100
101
  if self.__result is None:
101
102
  self.__result = await self.request()
102
- return self.__result
103
+ return self.__result # type: ignore
103
104
 
104
105
  def update_params(self, **kwargs) -> "Api":
105
106
  """
@@ -221,15 +222,13 @@ class Api:
221
222
  except aiohttp.ClientResponseError as e:
222
223
  raise NetworkException(e.status, e.message)
223
224
  return self.__process_response(resp, await resp.text())
224
- except aiohttp.client_exceptions.ClientConnectorError:
225
+ except aiohttp.ClientConnectionError:
225
226
  raise ClientException()
226
227
 
227
- def __process_response(
228
- self, resp: aiohttp.ClientResponse, resp_text: str
229
- ) -> dict | None:
228
+ def __process_response(self, resp: aiohttp.ClientResponse, resp_text: str) -> dict:
230
229
  content_length = resp.headers.get("content-length")
231
230
  if content_length and int(content_length) == 0:
232
- return None
231
+ raise ResponseException([self.module, self.method])
233
232
  try:
234
233
  resp_data = json.loads(resp_text)
235
234
  if self.module:
@@ -237,7 +236,7 @@ class Api:
237
236
  return request_data["data"]
238
237
  return resp_data
239
238
  except Exception:
240
- return None
239
+ raise ResponseException([self.module, self.method])
241
240
 
242
241
 
243
242
  @atexit.register
@@ -1,4 +0,0 @@
1
- from .api import album, mv, login, search, song, songlist, top
2
- from .utils.credential import Credential
3
-
4
- __all__ = ["album", "Credential", "login", "mv", "search", "song", "songlist", "top"]
@@ -1,121 +0,0 @@
1
- class ApiException(Exception):
2
- """
3
- API 异常基类
4
- """
5
-
6
- def __init__(self, msg: str = "未知原因"):
7
- super().__init__(msg)
8
- self.msg = msg
9
-
10
- def __str__(self):
11
- return self.msg
12
-
13
-
14
- class ArgsException(ApiException):
15
- """
16
- 参数错误。
17
- """
18
-
19
- def __init__(self, msg: str):
20
- super().__init__(msg)
21
- self.msg = msg
22
-
23
-
24
- class ClientException(ApiException):
25
- """
26
- 服务器连接错误
27
- """
28
-
29
- def __init__(self):
30
- super().__init__()
31
- self.msg = "连接到服务器时出现了问题"
32
-
33
-
34
- class NetworkException(ApiException):
35
- """
36
- 网络错误
37
- """
38
-
39
- def __init__(self, status: int, msg: str):
40
- super().__init__(msg)
41
- self.status = status
42
- self.msg = f"网络错误,状态码:{status} - {msg}"
43
-
44
- def __str__(self):
45
- return self.msg
46
-
47
-
48
- class ResponseCodeException(ApiException):
49
- """
50
- API 返回 code 错误
51
- """
52
-
53
- def __init__(self, code: int, subcode: int, api: list):
54
- super().__init__()
55
- self.msg = "API 返回 code 错误"
56
- self.code = code
57
- self.subcode = subcode
58
- self.api = api
59
-
60
- def __str__(self):
61
- return f"接口信息:{'.'.join(self.api)} 错误代码:{self.code} | {self.subcode}"
62
-
63
-
64
- class CredientialCanNotRefreshException(ApiException):
65
- """
66
- Crediential 无法刷新
67
- """
68
-
69
- def __init__(self):
70
- super().__init__()
71
- self.msg = "Crediential 无法刷新,请检查是否缺少必要字段"
72
-
73
-
74
- class CredentialNoMusicidException(ApiException):
75
- """
76
- Crediential 缺少 Musicid
77
- """
78
-
79
- def __init__(self):
80
- super().__init__()
81
- self.msg = "Crediential 缺少 Musicid"
82
-
83
-
84
- class CredentialNoMusickeyException(ApiException):
85
- """
86
- Crediential 缺少 Musickey
87
- """
88
-
89
- def __init__(self):
90
- super().__init__()
91
- self.msg = "Crediential 缺少 Musickey"
92
-
93
-
94
- class LoginDevicesFullException(ApiException):
95
- """
96
- 登录设备已满
97
- """
98
-
99
- def __init__(self):
100
- super().__init__()
101
- self.msg = "登录设备已满"
102
-
103
-
104
- class AuthcodeExpiredException(ApiException):
105
- """
106
- 验证码已失效
107
- """
108
-
109
- def __init__(self):
110
- super().__init__()
111
- self.msg = "验证码已失效"
112
-
113
-
114
- class AuthcodeGetFrequentlyException(ApiException):
115
- """
116
- 验证码获取频繁
117
- """
118
-
119
- def __init__(self):
120
- super().__init__()
121
- self.msg = "验证码获取频繁"