qqmusic-api-python 0.1.6__tar.gz → 0.1.7__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 (51) hide show
  1. {qqmusic_api_python-0.1.6 → qqmusic_api_python-0.1.7}/PKG-INFO +1 -1
  2. {qqmusic_api_python-0.1.6 → qqmusic_api_python-0.1.7}/pyproject.toml +1 -1
  3. {qqmusic_api_python-0.1.6 → qqmusic_api_python-0.1.7}/qqmusic_api/__init__.py +3 -2
  4. {qqmusic_api_python-0.1.6 → qqmusic_api_python-0.1.7}/qqmusic_api/album.py +4 -3
  5. qqmusic_api_python-0.1.7/qqmusic_api/data/api/lyric.json +25 -0
  6. {qqmusic_api_python-0.1.6 → qqmusic_api_python-0.1.7}/qqmusic_api/data/api/song.json +11 -0
  7. {qqmusic_api_python-0.1.6 → qqmusic_api_python-0.1.7}/qqmusic_api/data/file_type.json +4 -0
  8. {qqmusic_api_python-0.1.6 → qqmusic_api_python-0.1.7}/qqmusic_api/login.py +2 -2
  9. qqmusic_api_python-0.1.7/qqmusic_api/lyric.py +68 -0
  10. {qqmusic_api_python-0.1.6 → qqmusic_api_python-0.1.7}/qqmusic_api/search.py +6 -6
  11. {qqmusic_api_python-0.1.6 → qqmusic_api_python-0.1.7}/qqmusic_api/singer.py +3 -3
  12. {qqmusic_api_python-0.1.6 → qqmusic_api_python-0.1.7}/qqmusic_api/song.py +92 -30
  13. qqmusic_api_python-0.1.7/qqmusic_api/utils/tripledes.py +1089 -0
  14. qqmusic_api_python-0.1.7/qqmusic_api/utils/utils.py +95 -0
  15. {qqmusic_api_python-0.1.6 → qqmusic_api_python-0.1.7}/tests/test_song.py +2 -1
  16. qqmusic_api_python-0.1.6/qqmusic_api/utils/utils.py +0 -52
  17. {qqmusic_api_python-0.1.6 → qqmusic_api_python-0.1.7}/LICENSE +0 -0
  18. {qqmusic_api_python-0.1.6 → qqmusic_api_python-0.1.7}/README.md +0 -0
  19. {qqmusic_api_python-0.1.6 → qqmusic_api_python-0.1.7}/qqmusic_api/data/api/album.json +0 -0
  20. {qqmusic_api_python-0.1.6 → qqmusic_api_python-0.1.7}/qqmusic_api/data/api/login.json +0 -0
  21. {qqmusic_api_python-0.1.6 → qqmusic_api_python-0.1.7}/qqmusic_api/data/api/mv.json +0 -0
  22. {qqmusic_api_python-0.1.6 → qqmusic_api_python-0.1.7}/qqmusic_api/data/api/search.json +0 -0
  23. {qqmusic_api_python-0.1.6 → qqmusic_api_python-0.1.7}/qqmusic_api/data/api/singer.json +0 -0
  24. {qqmusic_api_python-0.1.6 → qqmusic_api_python-0.1.7}/qqmusic_api/data/api/songlist.json +0 -0
  25. {qqmusic_api_python-0.1.6 → qqmusic_api_python-0.1.7}/qqmusic_api/data/api/top.json +0 -0
  26. {qqmusic_api_python-0.1.6 → qqmusic_api_python-0.1.7}/qqmusic_api/data/search_type.json +0 -0
  27. {qqmusic_api_python-0.1.6 → qqmusic_api_python-0.1.7}/qqmusic_api/exceptions/ApiException.py +0 -0
  28. {qqmusic_api_python-0.1.6 → qqmusic_api_python-0.1.7}/qqmusic_api/exceptions/CredentialNoMusicidException.py +0 -0
  29. {qqmusic_api_python-0.1.6 → qqmusic_api_python-0.1.7}/qqmusic_api/exceptions/CredentialNoMusickeyException.py +0 -0
  30. {qqmusic_api_python-0.1.6 → qqmusic_api_python-0.1.7}/qqmusic_api/exceptions/CredentialNoRefreshkeyException.py +0 -0
  31. {qqmusic_api_python-0.1.6 → qqmusic_api_python-0.1.7}/qqmusic_api/exceptions/LoginException.py +0 -0
  32. {qqmusic_api_python-0.1.6 → qqmusic_api_python-0.1.7}/qqmusic_api/exceptions/ResponseCodeException.py +0 -0
  33. {qqmusic_api_python-0.1.6 → qqmusic_api_python-0.1.7}/qqmusic_api/exceptions/__init__.py +0 -0
  34. {qqmusic_api_python-0.1.6 → qqmusic_api_python-0.1.7}/qqmusic_api/mv.py +0 -0
  35. {qqmusic_api_python-0.1.6 → qqmusic_api_python-0.1.7}/qqmusic_api/songlist.py +0 -0
  36. {qqmusic_api_python-0.1.6 → qqmusic_api_python-0.1.7}/qqmusic_api/top.py +0 -0
  37. {qqmusic_api_python-0.1.6 → qqmusic_api_python-0.1.7}/qqmusic_api/utils/__init__.py +0 -0
  38. {qqmusic_api_python-0.1.6 → qqmusic_api_python-0.1.7}/qqmusic_api/utils/credential.py +0 -0
  39. {qqmusic_api_python-0.1.6 → qqmusic_api_python-0.1.7}/qqmusic_api/utils/network.py +0 -0
  40. {qqmusic_api_python-0.1.6 → qqmusic_api_python-0.1.7}/qqmusic_api/utils/qimei.py +0 -0
  41. {qqmusic_api_python-0.1.6 → qqmusic_api_python-0.1.7}/qqmusic_api/utils/sync.py +0 -0
  42. {qqmusic_api_python-0.1.6 → qqmusic_api_python-0.1.7}/tests/__init__.py +0 -0
  43. {qqmusic_api_python-0.1.6 → qqmusic_api_python-0.1.7}/tests/conftest.py +0 -0
  44. {qqmusic_api_python-0.1.6 → qqmusic_api_python-0.1.7}/tests/test_album.py +0 -0
  45. {qqmusic_api_python-0.1.6 → qqmusic_api_python-0.1.7}/tests/test_login.py +0 -0
  46. {qqmusic_api_python-0.1.6 → qqmusic_api_python-0.1.7}/tests/test_mv.py +0 -0
  47. {qqmusic_api_python-0.1.6 → qqmusic_api_python-0.1.7}/tests/test_qimei.py +0 -0
  48. {qqmusic_api_python-0.1.6 → qqmusic_api_python-0.1.7}/tests/test_search.py +0 -0
  49. {qqmusic_api_python-0.1.6 → qqmusic_api_python-0.1.7}/tests/test_singer.py +0 -0
  50. {qqmusic_api_python-0.1.6 → qqmusic_api_python-0.1.7}/tests/test_songlist.py +0 -0
  51. {qqmusic_api_python-0.1.6 → qqmusic_api_python-0.1.7}/tests/test_top.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: qqmusic-api-python
3
- Version: 0.1.6
3
+ Version: 0.1.7
4
4
  Summary: QQ音乐API封装库
5
5
  Keywords: music,api,qqmusic,tencentmusic
6
6
  Home-page: https://github.com/luren-dc/QQMusicApi
@@ -39,7 +39,7 @@ classifiers = [
39
39
  "Topic :: Software Development :: Libraries :: Python Modules",
40
40
  ]
41
41
  dynamic = []
42
- version = "0.1.6"
42
+ version = "0.1.7"
43
43
 
44
44
  [project.license]
45
45
  text = "MIT"
@@ -1,15 +1,16 @@
1
- from . import album, login, mv, search, singer, song, songlist, top
1
+ from . import album, login, lyric, mv, search, singer, song, songlist, top
2
2
  from .utils.credential import Credential
3
3
  from .utils.network import get_session, set_session
4
4
  from .utils.sync import sync
5
5
 
6
- __version__ = "0.1.6"
6
+ __version__ = "0.1.7"
7
7
 
8
8
  __all__ = [
9
9
  "album",
10
10
  "Credential",
11
11
  "get_session",
12
12
  "login",
13
+ "lyric",
13
14
  "mv",
14
15
  "search",
15
16
  "set_session",
@@ -22,9 +22,10 @@ class Album:
22
22
  mid: Optional[str] = None,
23
23
  id: Optional[int] = None,
24
24
  ):
25
- """/// admonition | 注意
26
- 歌曲 mid 和 id,两者至少提供一个
27
- ///
25
+ """初始化专辑类
26
+
27
+ Note:
28
+ 歌曲 mid 和 id,两者至少提供一个
28
29
 
29
30
  Args:
30
31
  mid: 专辑 mid
@@ -0,0 +1,25 @@
1
+ {
2
+ "info": {
3
+ "module": "music.musichallSong.PlayLyricInfo",
4
+ "method": "GetPlayLyricInfo",
5
+ "params": {
6
+ "ct": "int 11",
7
+ "cv": "int 13020508",
8
+ "lrc_t": "int 0",
9
+ "roma": "int 是否返回罗马歌词",
10
+ "roma_t": "int 0",
11
+ "trans": "int 是否返回翻译歌词",
12
+ "trans_t": "int 0",
13
+ "type": "int 1",
14
+ "albumName": "str 专辑名字",
15
+ "crypt": "int 是否加密歌词",
16
+ "qrc": "int 是否返回 qrc",
17
+ "qrc_t": "int 0",
18
+ "singerName": "str 歌手名字(取第一个)",
19
+ "songId": "int 歌曲 id",
20
+ "songMid": "str 歌曲 mid",
21
+ "songName": "str 歌曲名字"
22
+ },
23
+ "comment": "获取歌曲歌词信息"
24
+ }
25
+ }
@@ -81,6 +81,17 @@
81
81
  },
82
82
  "comment": "获取歌曲制作团队"
83
83
  },
84
+ "evkey": {
85
+ "module": "music.vkey.GetEVkey",
86
+ "method": "CgiGetEVkey",
87
+ "params": {
88
+ "filename": "[f'{file_type.s}{_}{_}{file_type.e}' for _ in mid]",
89
+ "guid": "str 随机32位字符串",
90
+ "songmid": "list mid 列表",
91
+ "songtype": "[1 for _ in range(len(mid))]"
92
+ },
93
+ "comment": "获取加密文件链接"
94
+ },
84
95
  "play_url": {
85
96
  "module": "music.vkey.GetVkey",
86
97
  "method": "UrlGetVkey",
@@ -15,6 +15,10 @@
15
15
  "s": "F000",
16
16
  "e": ".flac"
17
17
  },
18
+ "ogg_320": {
19
+ "s": "O800",
20
+ "e": ".ogg"
21
+ },
18
22
  "ogg_192": {
19
23
  "s": "O600",
20
24
  "e": ".ogg"
@@ -273,8 +273,8 @@ class QQLogin(QRCodeLogin):
273
273
  )
274
274
  location = res.headers.get("Location", "")
275
275
  code = re.findall(r"(?<=code=)(.+?)(?=&)", location)[0]
276
- res = await Api(**API["QQ_login"]).update_params(code=code).update_extra_common(tmeLoginType="2").result
277
- self.credential = Credential.from_cookies(res)
276
+ response = await Api(**API["QQ_login"]).update_params(code=code).update_extra_common(tmeLoginType="2").result
277
+ self.credential = Credential.from_cookies(response)
278
278
  return self.credential
279
279
 
280
280
 
@@ -0,0 +1,68 @@
1
+ """歌词 API"""
2
+
3
+ import xml.etree.ElementTree as ET
4
+ from typing import Optional
5
+
6
+ from .utils.network import Api
7
+ from .utils.utils import get_api, qrc_decrypt
8
+
9
+ API = get_api("lyric")
10
+
11
+
12
+ async def get_lyric(
13
+ *,
14
+ mid: Optional[str] = None,
15
+ id: Optional[int] = None,
16
+ qrc: bool = False,
17
+ trans: bool = False,
18
+ roma: bool = False,
19
+ ) -> dict[str, str]:
20
+ """获取歌词
21
+
22
+ Note:
23
+ 歌曲 mid 和 id,两者至少提供一个
24
+
25
+ Args:
26
+ mid: 歌曲 mid
27
+ id: 歌曲 id
28
+ qrc: 是否返回逐字歌词
29
+ trans: 是否返回翻译歌词
30
+ roma: 是否返回罗马歌词
31
+
32
+ Returns:
33
+ {"lyric": 歌词或逐字歌词, "trans": 翻译歌词, "roma": 罗马歌词}
34
+ """
35
+ if mid is None and id is None:
36
+ raise ValueError("mid or id must be provided")
37
+
38
+ params = {
39
+ "crypt": 1,
40
+ "ct": 11,
41
+ "cv": 13020508,
42
+ "lrc_t": 0,
43
+ "qrc": qrc,
44
+ "qrc_t": 0,
45
+ "roma": roma,
46
+ "roma_t": 0,
47
+ "songId": id,
48
+ "songMid": mid,
49
+ "trans": trans,
50
+ "trans_t": 0,
51
+ "type": 1,
52
+ }
53
+ res = await Api(**API["info"]).update_params(**params).result
54
+
55
+ lyric = qrc_decrypt(res["lyric"])
56
+
57
+ if lyric and qrc:
58
+ tree = ET.fromstring(lyric)
59
+ try:
60
+ lyric = tree.find(".//Lyric_0").attrib.get("LyricContent", "") # type: ignore
61
+ except AttributeError:
62
+ pass
63
+
64
+ return {
65
+ "lyric": lyric,
66
+ "trans": qrc_decrypt(res["trans"]),
67
+ "roma": qrc_decrypt(res["roma"]),
68
+ }
@@ -85,8 +85,8 @@ async def general_search(
85
85
 
86
86
  Args:
87
87
  keyword: 关键词
88
- page: 页码. Defaluts to 1
89
- highlight: 是否高亮关键词. Defaluts to True
88
+ page: 页码
89
+ highlight: 是否高亮关键词
90
90
 
91
91
  Returns:
92
92
  包含直接结果,歌曲,歌手,专辑,歌单,mv等.
@@ -114,10 +114,10 @@ async def search_by_type(
114
114
 
115
115
  Args:
116
116
  keyword: 关键词
117
- search_type: 搜索类型. Defaluts to SearchType.SONG
118
- num: 返回数量. Defaluts to 10
119
- page: 页码. Defaluts to 1
120
- highlight: 是否高亮关键词. Defaluts to True
117
+ search_type: 搜索类型
118
+ num: 返回数量
119
+ page: 页码
120
+ highlight: 是否高亮关键词
121
121
 
122
122
  Returns:
123
123
  搜索结果
@@ -212,9 +212,9 @@ class Singer:
212
212
  """获取歌手歌曲
213
213
 
214
214
  Args:
215
- type: Tab 类型. Defaluts to TabType.SONG
216
- page: 页码. Defaluts to 1
217
- num: 返回数量. Defaluts to 100
215
+ type: Tab 类型
216
+ page: 页码
217
+ num: 返回数量
218
218
 
219
219
  Returns:
220
220
  歌曲信息列表
@@ -19,6 +19,7 @@ class SongFileType(Enum):
19
19
  + NEW_1: 臻品全景声
20
20
  + NEW_2: 臻品音质2.0
21
21
  + FLAC: 无损音频压缩格式
22
+ + OGG_320: OGG 格式,320kbps,size_new[3]
22
23
  + OGG_192: OGG 格式,192kbps
23
24
  + OGG_96: OGG 格式,96kbps
24
25
  + MP3_320: MP3 格式,320kbps
@@ -26,13 +27,13 @@ class SongFileType(Enum):
26
27
  + ACC_192: AAC 格式,192kbps
27
28
  + ACC_96: AAC 格式,96kbps
28
29
  + ACC_48: AAC 格式,48kbps
29
- + TRY: 试听文件
30
30
  """
31
31
 
32
32
  NEW_0 = ("AI00", ".flac")
33
33
  NEW_1 = ("Q000", ".flac")
34
34
  NEW_2 = ("Q001", ".flac")
35
35
  FLAC = ("F000", ".flac")
36
+ OGG_320 = ("O800", ".ogg")
36
37
  OGG_192 = ("O600", ".ogg")
37
38
  OGG_96 = ("O400", ".ogg")
38
39
  MP3_320 = ("M800", ".mp3")
@@ -40,7 +41,6 @@ class SongFileType(Enum):
40
41
  ACC_192 = ("C600", ".m4a")
41
42
  ACC_96 = ("C400", ".m4a")
42
43
  ACC_48 = ("C200", ".m4a")
43
- TRY = ("RS02", ".mp3")
44
44
 
45
45
  def __init__(self, start_code: str, extension: str):
46
46
  self.__start_code = start_code
@@ -55,15 +55,37 @@ class SongFileType(Enum):
55
55
  return self.__extension
56
56
 
57
57
 
58
- class UrlType(Enum):
59
- """歌曲文件链接类型
58
+ class EncryptedSongFileType(Enum):
59
+ """加密歌曲文件类型
60
60
 
61
- + PLAY: 播放链接
62
- + DOWNLOAD: 下载链接
61
+ + NEW_0: 臻品母带2.0
62
+ + NEW_1: 臻品全景声
63
+ + NEW_2: 臻品音质2.0
64
+ + FLAC: 无损音频压缩格式
65
+ + OGG_320: OGG 格式,320kbps,size_new[3]
66
+ + OGG_192: OGG 格式,192kbps
67
+ + OGG_96: OGG 格式,96kbps
63
68
  """
64
69
 
65
- PLAY = "play_url"
66
- DOWNLOAD = "download_url"
70
+ NEW_0 = ("AIM0", ".mflac")
71
+ NEW_1 = ("Q0M0", ".mflac")
72
+ NEW_2 = ("Q0M1", ".mflac")
73
+ FLAC = ("F0M0", ".mflac")
74
+ OGG_320 = ("O800", ".mgg")
75
+ OGG_192 = ("O6M0", ".mgg")
76
+ OGG_96 = ("O4M0", ".mgg")
77
+
78
+ def __init__(self, start_code: str, extension: str):
79
+ self.__start_code = start_code
80
+ self.__extension = extension
81
+
82
+ @property
83
+ def s(self) -> str: # noqa : D102
84
+ return self.__start_code
85
+
86
+ @property
87
+ def e(self) -> str: # noqa: D102
88
+ return self.__extension
67
89
 
68
90
 
69
91
  class Song:
@@ -80,9 +102,10 @@ class Song:
80
102
  mid: Optional[str] = None,
81
103
  id: Optional[int] = None,
82
104
  ):
83
- """/// admonition | 注意
84
- 歌曲 mid 和 id,两者至少提供一个
85
- ///
105
+ """初始化歌曲类
106
+
107
+ Note:
108
+ 歌曲 mid 和 id,两者至少提供一个
86
109
 
87
110
  Args:
88
111
  mid: 歌曲 mid
@@ -167,9 +190,15 @@ class Song:
167
190
  Returns:
168
191
  MV信息
169
192
  """
170
- return (await Api(**API["mv"]).update_params(songid=str(await self.get_id()), songtype=1, lastvid=0).result)[
171
- "list"
172
- ]
193
+ return (
194
+ await Api(**API["mv"])
195
+ .update_params(
196
+ songid=str(await self.get_id()),
197
+ songtype=1,
198
+ lastvid=0,
199
+ )
200
+ .result
201
+ )["list"]
173
202
 
174
203
  async def get_other_version(self) -> list[dict]:
175
204
  """获取歌曲其他版本
@@ -197,21 +226,19 @@ class Song:
197
226
 
198
227
  async def get_url(
199
228
  self,
200
- file_type: SongFileType = SongFileType.MP3_128,
201
- url_type: UrlType = UrlType.PLAY,
229
+ file_type: Union[SongFileType, EncryptedSongFileType] = SongFileType.MP3_128,
202
230
  credential: Optional[Credential] = None,
203
231
  ) -> str:
204
232
  """获取歌曲文件链接
205
233
 
206
234
  Args:
207
- file_type: 歌曲文件类型. Defaults to SongFileType.MP3_128
208
- url_type: 歌曲链接类型. Defaults to UrlType.PLAY
209
- credential: 账号凭证. Defaults to None
235
+ file_type: 歌曲文件类型
236
+ credential: 账号凭证
210
237
 
211
238
  Returns:
212
239
  链接字典
213
240
  """
214
- return (await get_song_urls([await self.get_mid()], file_type, url_type, credential))[self.mid]
241
+ return (await get_song_urls([await self.get_mid()], file_type, credential))[self.mid][0]
215
242
 
216
243
 
217
244
  async def query_song(value: Union[list[str], list[int]]) -> list[dict]:
@@ -242,26 +269,26 @@ async def query_song(value: Union[list[str], list[int]]) -> list[dict]:
242
269
 
243
270
  async def get_song_urls(
244
271
  mid: list[str],
245
- file_type: SongFileType = SongFileType.MP3_128,
246
- url_type: UrlType = UrlType.PLAY,
272
+ file_type: Union[EncryptedSongFileType, SongFileType] = SongFileType.MP3_128,
247
273
  credential: Optional[Credential] = None,
248
- ) -> dict[str, str]:
274
+ ) -> Union[dict[str, str], dict[str, tuple[str, str]]]:
249
275
  """获取歌曲文件链接
250
276
 
251
277
  Args:
252
278
  mid: 歌曲 mid
253
- file_type: 歌曲文件类型. Defaults to SongFileType.MP3_128
254
- url_type: 歌曲链接类型. Defaults to UrlType.PLAY
255
- credential: Credential 类. Defaluts to None
279
+ file_type: 歌曲文件类型
280
+ credential: Credential
256
281
 
257
282
  Returns:
258
- 链接字典
283
+ 返回链接字典,加密歌曲返回 `ekey` 用于解密
259
284
  """
285
+ encrypted = isinstance(file_type, EncryptedSongFileType)
260
286
  # 分割 id,单次最大请求100
261
287
  mid_list = [mid[i : i + 100] for i in range(0, len(mid), 100)]
262
288
  # 选择文件域名
263
- domain = "https://isure.stream.qqmusic.qq.com/" if url_type == UrlType.PLAY else "https://dl.stream.qqmusic.qq.com/"
264
- api = Api(**API[url_type.value], credential=credential or Credential())
289
+ domain = "https://isure.stream.qqmusic.qq.com/"
290
+ api_data = API["play_url"] if not encrypted else API["evkey"]
291
+ api = Api(**api_data, credential=credential or Credential())
265
292
  urls = {}
266
293
 
267
294
  async def get_song_url(mid):
@@ -278,7 +305,42 @@ async def get_song_urls(
278
305
  data = res["midurlinfo"]
279
306
  for info in data:
280
307
  song_url = domain + info["wifiurl"] if info["wifiurl"] else ""
281
- urls[info["songmid"]] = song_url
308
+ if not encrypted:
309
+ urls[info["songmid"]] = song_url
310
+ else:
311
+ urls[info["songmid"]] = (song_url, info["ekey"])
282
312
 
283
313
  await asyncio.gather(*[asyncio.create_task(get_song_url(mid)) for mid in mid_list])
284
314
  return urls
315
+
316
+
317
+ async def get_try_url(mid: str, vs: str) -> Optional[str]:
318
+ """获取试听文件链接
319
+
320
+ Tips:
321
+ 使用 `size_try` 字段判断是否存在试听文件
322
+ 参数 `vs` 请传入歌曲信息 `vs` 字段第一个
323
+
324
+ Args:
325
+ mid: 歌曲 mid
326
+ vs: 歌曲 vs
327
+
328
+ Returns:
329
+ 试听文件链接
330
+ """
331
+ res = await (
332
+ Api(**API["play_url"])
333
+ .update_params(
334
+ filename=[f"RS02{vs}.mp3"],
335
+ guid="".join(
336
+ random.choices("abedf1234567890", k=32),
337
+ ),
338
+ songmid=[mid],
339
+ songtype=[1],
340
+ )
341
+ .result
342
+ )
343
+ if url := res["midurlinfo"][0]["wifiurl"]:
344
+ return f"https://isure.stream.qqmusic.qq.com/{url}"
345
+ else:
346
+ return None