qqmusic-api-python 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,168 @@
1
+ from enum import Enum
2
+
3
+ from qqmusic_api.api.song import Song
4
+
5
+ from ..utils.common import get_api, parse_song_info, random_searchID
6
+ from ..utils.network import Api
7
+
8
+ API = get_api("search")
9
+
10
+
11
+ class SearchType(Enum):
12
+ """
13
+ 搜索类型
14
+
15
+ + SONG: 歌曲
16
+ + SINGER: 歌手
17
+ + ALBUM: 专辑
18
+ + SONGLIST: 歌单
19
+ + MV: MV
20
+ + LYRIC: 歌词
21
+ + USER: 用户
22
+ + AUDIO_SONG: 节目专辑
23
+ + AUDIO: 节目
24
+ """
25
+
26
+ SONG = 0
27
+ SINGER = 1
28
+ ALBUM = 2
29
+ SONGLIST = 3
30
+ MV = 4
31
+ LYRIC = 7
32
+ USER = 8
33
+ AUDIO_ALBUM = 15
34
+ AUDIO = 18
35
+
36
+
37
+ async def hotkey() -> list[dict]:
38
+ """
39
+ 获取热搜词
40
+
41
+ Returns:
42
+ list: 热搜词列表,k为热搜词,n为搜索量
43
+ """
44
+ params = {"search_id": random_searchID()}
45
+ res = await Api(**API["hotkey"]).update_params(**params).result
46
+ data = res.get("vec_hotkey", [])
47
+ return [{"k": hotkey["query"], "n": hotkey["score"]} for hotkey in data]
48
+
49
+
50
+ async def complete(keyword: str, highlight: bool = False) -> list[str]:
51
+ """
52
+ 搜索词补全
53
+
54
+ Args:
55
+ keyword: 关键词
56
+ highlight: 是否高亮关键词. Defaluts to False
57
+
58
+ Returns:
59
+ list: 补全结果
60
+ """
61
+ params = {
62
+ "search_id": random_searchID(),
63
+ "query": keyword,
64
+ "num_per_page": 0,
65
+ "page_idx": 0,
66
+ }
67
+ res = await Api(**API["complete"]).update_params(**params).result
68
+ data = res["items"]
69
+ if highlight:
70
+ return [item["hint_hilight"] for item in data]
71
+ else:
72
+ return [item["hint"] for item in data]
73
+
74
+
75
+ async def quick_search(keyword: str) -> dict:
76
+ """
77
+ 快速搜索
78
+
79
+ Args:
80
+ keyword: 关键词
81
+
82
+ Returns:
83
+ dict: 包含专辑,歌手,歌曲的简略信息
84
+ """
85
+ res = await Api(**API["quick_search"]).update_params(key=keyword).result
86
+ return res["data"]
87
+
88
+
89
+ async def general_search(keyword: str, page: int = 1, highlight: bool = False) -> dict:
90
+ """
91
+ 综合搜索
92
+
93
+ Args:
94
+ keyword: 关键词
95
+ page: 页码. Defaluts to 1
96
+ highlight: 是否高亮关键词. Defaluts to False
97
+
98
+ Returns:
99
+ dict: 包含直接结果,歌曲,歌手,专辑,歌单,mv等.
100
+ """
101
+ params = {
102
+ "search_id": random_searchID(),
103
+ "search_type": 100,
104
+ "query": keyword,
105
+ "page_id": page,
106
+ "highlight": highlight,
107
+ "grp": 1,
108
+ }
109
+ res = await Api(**API["general_search"]).update_params(**params).result
110
+ data = res["body"]
111
+ return {
112
+ "direct": data["direct_result"]["direct_group"],
113
+ "song": [parse_song_info(d) for d in data["item_song"]["items"]],
114
+ "singer": data["singer"]["items"],
115
+ "album": data["item_album"]["items"],
116
+ "songlist": data["item_songlist"]["items"],
117
+ "mv": data["item_mv"]["items"],
118
+ }
119
+
120
+
121
+ async def search_by_type(
122
+ keyword: str,
123
+ search_type: SearchType = SearchType.SONG,
124
+ num: int = 10,
125
+ page: int = 1,
126
+ selectors: dict = {},
127
+ highlight: bool = False,
128
+ ) -> list[Song]:
129
+ """
130
+ 搜索
131
+
132
+ Args:
133
+ keyword: 关键词
134
+ search_type: 搜索类型. Defaluts to SearchType.SONG
135
+ num: 返回数量. Defaluts to 10
136
+ page: 页码. Defaluts to 1
137
+ selectors: 选择器. Defaluts to {}
138
+ highlight: 是否高亮关键词. Defaluts to False
139
+
140
+ Returns:
141
+ list: 搜索结果.
142
+ """
143
+ params = {
144
+ "searchid": random_searchID(),
145
+ "query": keyword,
146
+ "search_type": search_type.value,
147
+ "num_per_page": num,
148
+ "page_num": page,
149
+ "highlight": highlight,
150
+ "page_id": page,
151
+ "selectors": selectors,
152
+ }
153
+ res = await Api(**API["desktop_search_by_type"]).update_params(**params).result
154
+ types = {
155
+ SearchType.SONG: "song",
156
+ SearchType.SINGER: "singer",
157
+ SearchType.ALBUM: "album",
158
+ SearchType.SONGLIST: "songlist",
159
+ SearchType.MV: "mv",
160
+ SearchType.LYRIC: "song",
161
+ SearchType.USER: "user",
162
+ SearchType.AUDIO_ALBUM: "album",
163
+ SearchType.AUDIO: "song",
164
+ }
165
+ data = res["body"][types[search_type]]["list"]
166
+ if search_type in [SearchType.SONG, SearchType.LYRIC, SearchType.AUDIO]:
167
+ return [Song.from_dict(song) for song in data]
168
+ return data
@@ -0,0 +1,389 @@
1
+ from enum import Enum
2
+ from typing import Optional
3
+
4
+ from ..exceptions import ArgsException
5
+ from ..utils.common import get_api, parse_song_info, random_string
6
+ from ..utils.credential import Credential
7
+ from ..utils.network import Api
8
+
9
+ API = get_api("song")
10
+
11
+
12
+ class SongFileType(Enum):
13
+ """
14
+ 歌曲文件类型
15
+
16
+ + NEW_0: 臻品母带2.0
17
+ + NEW_1: 臻品全景声
18
+ + NEW_2: 臻品音质2.0
19
+ + FLAC: 无损音频压缩格式
20
+ + OGG_192: OGG 格式,192kbps
21
+ + OGG_96: OGG 格式,96kbps
22
+ + MP3_320: MP3 格式,320kbps
23
+ + MP3_128: MP3 格式,128kbps
24
+ + ACC_192: AAC 格式,192kbps
25
+ + ACC_96: AAC 格式,96kbps
26
+ + ACC_48: AAC 格式,48kbps
27
+ + TRY: 试听文件
28
+ """
29
+
30
+ NEW_0 = ("AI00", ".flac")
31
+ NEW_1 = ("Q000", ".flac")
32
+ NEW_2 = ("Q001", ".flac")
33
+ FLAC = ("F000", ".flac")
34
+ OGG_192 = ("O600", ".ogg")
35
+ OGG_96 = ("O400", ".ogg")
36
+ MP3_320 = ("M800", ".mp3")
37
+ MP3_128 = ("M500", ".mp3")
38
+ ACC_192 = ("C600", ".m4a")
39
+ ACC_96 = ("C400", ".m4a")
40
+ ACC_48 = ("C200", ".m4a")
41
+ TRY = ("RS02", ".mp3")
42
+
43
+ def __init__(self, start_code: str, extension: str):
44
+ self.__start_code = start_code
45
+ self.__extension = extension
46
+
47
+ @property
48
+ def s(self) -> str:
49
+ return self.__start_code
50
+
51
+ @property
52
+ def e(self) -> str:
53
+ return self.__extension
54
+
55
+
56
+ class UrlType(Enum):
57
+ """
58
+ 歌曲文件链接类型
59
+ + PLAY: 播放链接
60
+ + DOWNLOAD: 下载链接
61
+ """
62
+
63
+ PLAY = "play_url"
64
+ DOWNLOAD = "download_url"
65
+
66
+
67
+ class Song:
68
+ """
69
+ 歌曲类
70
+ """
71
+
72
+ def __init__(
73
+ self,
74
+ mid: Optional[str] = None,
75
+ id: Optional[int] = None,
76
+ credential: Optional[Credential] = None,
77
+ ):
78
+ """
79
+ Args:
80
+ mid: 歌曲 mid. 歌曲 id 和歌曲 mid 必须提供其中之一
81
+ id: 歌曲 id. 歌曲 id 和歌曲 mid 必须提供其中之一
82
+ credential: Credential 类. Defaluts to None
83
+ """
84
+ # ID 检查
85
+ if mid is None and id is None:
86
+ raise ArgsException("请至少提供 mid 和 id 中的其中一个参数。")
87
+ self._mid = mid
88
+ self._id = id
89
+ self.credential = Credential() if credential is None else credential
90
+ self._info: Optional[dict] = None
91
+
92
+ @classmethod
93
+ def from_dict(cls, info: dict):
94
+ """
95
+ 从字典新建 Song
96
+
97
+ Args:
98
+ info: 歌曲字典
99
+
100
+ Returns:
101
+ Song: 歌曲类
102
+ """
103
+ info = parse_song_info(info)
104
+ s = cls(id=info["info"]["id"], mid=info["info"]["mid"])
105
+ s._info = info
106
+ return s
107
+
108
+ async def __get_info(self):
109
+ """
110
+ 获取歌曲必要信息
111
+ """
112
+ if not self._info:
113
+ if self._mid:
114
+ self._info = (await query_by_mid([self._mid]))[0]
115
+ elif self._id:
116
+ self._info = (await query_by_id([self._id]))[0]
117
+ return self._info
118
+
119
+ @property
120
+ async def mid(self) -> str:
121
+ """
122
+ 获取歌曲 mid
123
+
124
+ Returns:
125
+ str: mid
126
+ """
127
+ if not self._mid:
128
+ self._mid = (await self.__get_info())["info"]["mid"]
129
+ return str(self._mid)
130
+
131
+ @property
132
+ async def id(self) -> int:
133
+ """
134
+ 获取歌曲 id
135
+
136
+ Returns:
137
+ int: id
138
+ """
139
+ if not self._id:
140
+ self._id = (await self.__get_info())["info"]["id"]
141
+ return int(self._id)
142
+
143
+ async def __prepare_param(self, is_mid: bool = False, is_id: bool = False) -> dict:
144
+ """
145
+ 准备请求参数
146
+
147
+ Args:
148
+ is_mid: 是否强制使用 mid. Defaults to False
149
+ is_id: 是否强制使用 id. Defaults to False
150
+
151
+ Returns:
152
+ dict: 请求参数
153
+ """
154
+ if is_mid and is_id:
155
+ raise ArgsException("参数错误")
156
+ if is_mid:
157
+ return {"songmid": await self.mid}
158
+ elif is_id:
159
+ return {"songid": await self.id}
160
+ elif self._mid:
161
+ return {"songmid": self._mid}
162
+ elif self._id:
163
+ return {"songid": self._id}
164
+ else:
165
+ return {}
166
+
167
+ async def get_info(self) -> dict:
168
+ """
169
+ 获取歌曲基本信息
170
+
171
+ Returns:
172
+ dict: 基本信息
173
+ """
174
+ return (await self.__get_info())["info"]
175
+
176
+ async def get_detail(self) -> dict:
177
+ """
178
+ 获取歌曲详细信息
179
+
180
+ Returns:
181
+ dict: 详细信息
182
+ """
183
+ param = await self.__prepare_param()
184
+ if "songmid" in param:
185
+ param["song_mid"] = param.pop("songmid")
186
+ if "songid" in param:
187
+ param["song_id"] = param.pop("songid")
188
+ return await Api(**API["detail"]).update_params(**param).result
189
+
190
+ async def get_similar_song(self) -> list[dict]:
191
+ """
192
+ 获取歌曲相似歌曲
193
+
194
+ Returns:
195
+ list: 歌曲信息
196
+ """
197
+ param = await self.__prepare_param(is_id=True)
198
+ res = await Api(**API["similar"]).update_params(**param).result
199
+ return [parse_song_info(song["track"]) for song in res["vecSong"]]
200
+
201
+ async def get_labels(self) -> list[dict]:
202
+ """
203
+ 获取歌曲标签
204
+
205
+ Returns:
206
+ list: 标签信息
207
+ """
208
+ param = await self.__prepare_param(is_id=True)
209
+ return (await Api(**API["labels"]).update_params(**param).result)["labels"]
210
+
211
+ async def get_related_playlist(self) -> list[dict]:
212
+ """
213
+ 获取歌曲相关歌单
214
+
215
+ Returns:
216
+ list: 歌单信息
217
+ """
218
+ param = await self.__prepare_param(is_id=True)
219
+ return (await Api(**API["playlist"]).update_params(**param).result)[
220
+ "vecPlaylist"
221
+ ]
222
+
223
+ async def get_related_mv(self) -> list[dict]:
224
+ """
225
+ 获取歌曲相关MV
226
+
227
+ Returns:
228
+ list: MV信息
229
+ """
230
+ param = await self.__prepare_param()
231
+ return (await Api(**API["mv"]).update_params(**param).result)["list"]
232
+
233
+ async def get_other_version(self) -> list[dict]:
234
+ """
235
+ 获取歌曲其他版本
236
+
237
+ Returns:
238
+ list: 歌曲信息
239
+ """
240
+ param = await self.__prepare_param()
241
+ res = await Api(**API["other"]).update_params(**param).result
242
+ return [parse_song_info(song) for song in res["versionList"]]
243
+
244
+ async def get_sheet(self) -> list[dict]:
245
+ """
246
+ 获取歌曲相关曲谱
247
+
248
+ Returns:
249
+ list: 曲谱信息
250
+ """
251
+ param = await self.__prepare_param(is_mid=True)
252
+ param["scoreType"] = -1
253
+ return (await Api(**API["sheet"]).update_params(**param).result)["result"]
254
+
255
+ async def get_producer(self) -> list[dict]:
256
+ """
257
+ 获取歌曲制作信息
258
+
259
+ Returns:
260
+ list: 人员信息
261
+ """
262
+ param = await self.__prepare_param()
263
+ return (await Api(**API["producer"]).update_params(**param).result)["Lst"]
264
+
265
+ async def get_url(
266
+ self,
267
+ file_type: SongFileType = SongFileType.MP3_128,
268
+ url_type: UrlType = UrlType.PLAY,
269
+ ) -> dict[str, str]:
270
+ """
271
+ 获取歌曲文件链接
272
+
273
+ Args:
274
+ file_type: 歌曲文件类型. Defaults to SongFileType.MP3_128
275
+ url_type: 歌曲链接类型. Defaults to UrlType.PLAY
276
+
277
+ Returns:
278
+ dict: 链接字典
279
+ """
280
+ return await get_song_urls([await self.mid], file_type, url_type)
281
+
282
+ async def get_file_size(self, file_type: Optional[SongFileType] = None) -> dict:
283
+ """
284
+ 获取歌曲文件大小
285
+
286
+ Args:
287
+ file_type: 指定文件类型. Defaults to None
288
+
289
+ Return:
290
+ dict: 文件大小
291
+ """
292
+ size = (await self.__get_info())["file"]
293
+ if file_type:
294
+ name = file_type.name.lower()
295
+ size = {name: size[name]}
296
+ return size
297
+
298
+
299
+ async def query_by_id(id: list[int]) -> list[dict]:
300
+ """
301
+ 根据 mid 获取歌曲信息
302
+
303
+ Args:
304
+ mid: 歌曲 mid 列表
305
+
306
+ Returns:
307
+ list: 歌曲信息
308
+ """
309
+ param = {
310
+ "ids": id,
311
+ "mids": [],
312
+ "types": [0 for _ in range(len(id))],
313
+ "modify_stamp": [0 for _ in range(len(id))],
314
+ "ctx": 0,
315
+ "client": 1,
316
+ }
317
+ res = await Api(**API["query"]).update_params(**param).result
318
+ tracks = res["tracks"]
319
+ return [parse_song_info(song) for song in tracks]
320
+
321
+
322
+ async def query_by_mid(mid: list[str]) -> list[dict]:
323
+ """
324
+ 根据 mid 获取歌曲信息
325
+
326
+ Args:
327
+ mid: 歌曲 mid 列表
328
+
329
+ Returns:
330
+ list: 歌曲信息
331
+ """
332
+ param = {
333
+ "ids": [],
334
+ "mids": mid,
335
+ "types": [0 for _ in range(len(mid))],
336
+ "modify_stamp": [0 for _ in range(len(mid))],
337
+ "ctx": 0,
338
+ "client": 1,
339
+ }
340
+ res = await Api(**API["query"]).update_params(**param).result
341
+ tracks = res["tracks"]
342
+ return [parse_song_info(song) for song in tracks]
343
+
344
+
345
+ async def get_song_urls(
346
+ mid: list[str],
347
+ file_type: SongFileType = SongFileType.MP3_128,
348
+ url_type: UrlType = UrlType.PLAY,
349
+ credential: Optional[Credential] = None,
350
+ ) -> dict[str, str]:
351
+ """
352
+ 获取歌曲文件链接
353
+
354
+ Args:
355
+ mid: 歌曲 mid
356
+ file_type: 歌曲文件类型. Defaults to SongFileType.MP3_128
357
+ url_type: 歌曲链接类型. Defaults to UrlType.PLAY
358
+ credential: Credential 类. Defaluts to None
359
+
360
+ Returns:
361
+ dict: 链接字典
362
+ """
363
+ if credential is None:
364
+ credential = Credential()
365
+ # 分割 id,单次最大请求100
366
+ mid_list = [mid[i : i + 100] for i in range(0, len(mid), 100)]
367
+ # 选择文件域名
368
+ domain = (
369
+ "https://isure.stream.qqmusic.qq.com/"
370
+ if url_type == UrlType.PLAY
371
+ else "https://dl.stream.qqmusic.qq.com/"
372
+ )
373
+ api = Api(**API[url_type.value], credential=credential)
374
+ urls = {}
375
+ 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
389
+ return urls
@@ -0,0 +1,81 @@
1
+ from typing import Optional
2
+
3
+ from qqmusic_api.api.song import Song
4
+
5
+ from ..utils.common import get_api
6
+ from ..utils.network import Api
7
+
8
+ API = get_api("songlist")
9
+
10
+
11
+ class Songlist:
12
+ """
13
+ 歌单类
14
+ """
15
+
16
+ def __init__(self, id: int):
17
+ """
18
+ Args:
19
+ id: 歌单 ID
20
+ """
21
+ self.id = id
22
+ self._info: Optional[dict] = None
23
+
24
+ async def __get_info(self):
25
+ if not self._info:
26
+ param = {
27
+ "disstid": self.id,
28
+ "dirid": 0,
29
+ "tag": True,
30
+ "song_num": 0,
31
+ "userinfo": True,
32
+ "orderlist": True,
33
+ }
34
+ result = (
35
+ await Api(**API["detail"])
36
+ .update_params(**param)
37
+ .update_headers(Referer="")
38
+ .result
39
+ )
40
+ self._info = result # type: ignore
41
+ return self._info
42
+
43
+ async def get_detail(self) -> dict:
44
+ """
45
+ 获取歌单详细信息
46
+
47
+ Returns:
48
+ dict: 歌单信息
49
+ """
50
+ result = await self.__get_info()
51
+ return result["dirinfo"]
52
+
53
+ async def get_song(self) -> list[Song]:
54
+ """
55
+ 获取歌单歌曲
56
+
57
+ Returns:
58
+ list: 歌单歌曲
59
+ """
60
+ result = await self.__get_info()
61
+ return [Song.from_dict(song) for song in result["songlist"]]
62
+
63
+ async def get_song_tag(self) -> list[dict]:
64
+ """
65
+ 获取歌单歌曲标签
66
+
67
+ Returns:
68
+ list: 歌单歌曲标签
69
+ """
70
+ result = await self.__get_info()
71
+ return result["songtag"]
72
+
73
+ async def get_song_mid(self) -> list[dict]:
74
+ """
75
+ 获取歌单歌曲全部 mid
76
+
77
+ Returns:
78
+ list: 歌单歌曲 mid
79
+ """
80
+ result = await self.__get_info()
81
+ return [song["mid"] for song in result["songlist"]]