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,4 @@
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"]
@@ -0,0 +1,40 @@
1
+ from qqmusic_api.api.song import Song
2
+
3
+ from ..utils.common import get_api
4
+ from ..utils.network import Api
5
+
6
+ API = get_api("album")
7
+
8
+
9
+ class Album:
10
+ """
11
+ 专辑类
12
+ """
13
+
14
+ def __init__(self, mid: str):
15
+ """
16
+ Args:
17
+ mid: 专辑 mid
18
+ """
19
+ self.mid = mid
20
+
21
+ async def get_detail(self) -> dict:
22
+ """
23
+ Returns:
24
+ dict: 专辑详细信息
25
+ """
26
+ return await Api(**API["detail"]).update_params(albumMid=self.mid).result
27
+
28
+ async def get_song(self) -> list[Song]:
29
+ """
30
+ 获取专辑歌曲
31
+
32
+ Returns:
33
+ list: 歌曲列表
34
+ """
35
+ result = (
36
+ await Api(**API["song"])
37
+ .update_params(albumMid=self.mid, begin=0, num=0)
38
+ .result
39
+ )
40
+ return [Song.from_dict(song["songInfo"]) for song in result["songList"]]
@@ -0,0 +1,437 @@
1
+ import random
2
+ import re
3
+ import time
4
+ from abc import ABC, abstractmethod
5
+ from enum import Enum
6
+ from typing import Optional
7
+
8
+ from ..exceptions import AuthcodeExpiredException, ResponseCodeException
9
+ from ..utils.common import get_api, hash33, random_uuid
10
+ from ..utils.credential import Credential
11
+ from ..utils.network import Api, get_aiohttp_session
12
+
13
+ API = get_api("login")
14
+
15
+
16
+ class QrCodeLoginEvents(Enum):
17
+ """
18
+ 二维码登录状态枚举
19
+
20
+ + SCAN: 未扫描二维码
21
+ + CONF: 未确认登录
22
+ + TIMEOUT: 二维码过期
23
+ + DONE: 成功
24
+ + REFUSE: 拒绝登录
25
+ + OTHER: 未知情况
26
+ """
27
+
28
+ DONE = 0
29
+ SCAN = 1
30
+ CONF = 2
31
+ TIMEOUT = 3
32
+ REFUSE = 4
33
+ OTHER = 5
34
+
35
+
36
+ class PhoneLoginEvents(Enum):
37
+ """
38
+ 手机登录状态枚举
39
+
40
+ + SEND: 发送成功
41
+ + CAPTCHA: 需要滑块验证
42
+ + OTHER: 未知情况
43
+ """
44
+
45
+ SEND = 0
46
+ CAPTCHA = 1
47
+ OTHER = 5
48
+
49
+
50
+ class Login(ABC):
51
+ """登录抽象类"""
52
+
53
+ def __init__(self) -> None:
54
+ super().__init__()
55
+ self.musicid = ""
56
+ self.auth_url = ""
57
+ self.state: Optional[QrCodeLoginEvents] = None
58
+ self.qrcode_data: Optional[bytes] = None
59
+ self.credential: Optional[Credential] = None
60
+
61
+ async def __aenter__(self):
62
+ self.session = get_aiohttp_session()
63
+ return self
64
+
65
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
66
+ await self.close()
67
+
68
+ async def close(self):
69
+ if not self.session.closed:
70
+ await self.session.close()
71
+
72
+ def initialized(self) -> bool:
73
+ return bool(
74
+ self.qrcode_data
75
+ and self.state not in [QrCodeLoginEvents.TIMEOUT, QrCodeLoginEvents.REFUSE]
76
+ )
77
+
78
+ @abstractmethod
79
+ async def get_qrcode(self) -> bytes:
80
+ """
81
+ 获取二维码
82
+
83
+ Returns:
84
+ bytes: 二维码图像数据
85
+ """
86
+
87
+ @abstractmethod
88
+ async def get_qrcode_state(self) -> QrCodeLoginEvents:
89
+ """
90
+ 获取二维码状态
91
+
92
+ Returns:
93
+ QrCodeLoginEvents: 二维码状态
94
+ """
95
+
96
+ @abstractmethod
97
+ async def authorize(self, authcode: Optional[int] = None) -> Credential:
98
+ """
99
+ 登录鉴权
100
+
101
+ Args:
102
+ code: 验证码. Defaluts to None
103
+
104
+ Returns:
105
+ Credential: 用户凭证
106
+ """
107
+
108
+ @abstractmethod
109
+ async def send_authcode(self) -> PhoneLoginEvents:
110
+ """
111
+ 发送验证码
112
+
113
+ Returns:
114
+ PhoneLoginEvents: 操作状态
115
+ """
116
+
117
+
118
+ class QQLogin(Login):
119
+ """QQ登录"""
120
+
121
+ async def send_authcode(self):
122
+ raise NotImplementedError("不支持")
123
+
124
+ async def get_qrcode(self):
125
+ if self.initialized():
126
+ return self.qrcode_data
127
+ async with self.session.get(
128
+ "https://xui.ptlogin2.qq.com/cgi-bin/xlogin",
129
+ params={
130
+ "appid": "716027609",
131
+ "daid": "383",
132
+ "style": "33",
133
+ "login_text": "登录",
134
+ "hide_title_bar": "1",
135
+ "hide_border": "1",
136
+ "target": "self",
137
+ "s_url": "https://graph.qq.com/oauth2.0/login_jump",
138
+ "pt_3rd_aid": "100497308",
139
+ "pt_feedback_link": "https://support.qq.com/products/77942?customInfo=.appid100497308",
140
+ "theme": "2",
141
+ "verify_theme": "",
142
+ },
143
+ ) as res:
144
+ self.sig = res.cookies.get("pt_login_sig").value
145
+ async with self.session.get(
146
+ "https://ssl.ptlogin2.qq.com/ptqrshow",
147
+ params={
148
+ "appid": "716027609",
149
+ "e": "2",
150
+ "l": "M",
151
+ "s": "3",
152
+ "d": "72",
153
+ "v": "4",
154
+ "t": str(random.random()),
155
+ "daid": "383",
156
+ "pt_3rd_aid": "100497308",
157
+ },
158
+ ) as res:
159
+ self.ptqrtoken = hash33(res.cookies.get("qrsig").value)
160
+ self.qrcode_data = await res.read()
161
+ return self.qrcode_data
162
+
163
+ async def get_qrcode_state(self):
164
+ async with self.session.get(
165
+ "https://ssl.ptlogin2.qq.com/ptqrlogin",
166
+ params={
167
+ "u1": "https://graph.qq.com/oauth2.0/login_jump",
168
+ "ptqrtoken": self.ptqrtoken,
169
+ "ptredirect": "0",
170
+ "h": "1",
171
+ "t": "1",
172
+ "g": "1",
173
+ "from_ui": "1",
174
+ "ptlang": "2052",
175
+ "action": "0-0-%s" % int(time.time() * 1000),
176
+ "js_ver": "20102616",
177
+ "js_type": "1",
178
+ "login_sig": self.sig,
179
+ "pt_uistyle": "40",
180
+ "aid": "716027609",
181
+ "daid": "383",
182
+ "pt_3rd_aid": "100497308",
183
+ "has_onekey": "1",
184
+ },
185
+ ) as res:
186
+ data = await res.text()
187
+ text_to_state = {
188
+ "二维码未失效": QrCodeLoginEvents.SCAN,
189
+ "二维码认证中": QrCodeLoginEvents.CONF,
190
+ "二维码已失效": QrCodeLoginEvents.TIMEOUT,
191
+ "本次登录已被拒绝": QrCodeLoginEvents.REFUSE,
192
+ "登录成功": QrCodeLoginEvents.DONE,
193
+ }
194
+ state = QrCodeLoginEvents.OTHER
195
+ for text, value in text_to_state.items():
196
+ if text in data:
197
+ state = value
198
+ break
199
+ self.state = state
200
+ if state == QrCodeLoginEvents.DONE:
201
+ self.musicid = re.findall(r"&uin=(.+?)&service", data)[0]
202
+ self.auth_url = re.findall(r"'(https:.*?)'", data)[0]
203
+ return state
204
+
205
+ async def authorize(self, authcode: Optional[int] = None):
206
+ if self.credential:
207
+ return self.credential
208
+ async with self.session.get(self.auth_url, allow_redirects=False) as res:
209
+ from http import cookies
210
+
211
+ set_cookie_header = res.headers.getall("Set-Cookie", [])
212
+ for header in set_cookie_header:
213
+ cookie: cookies.SimpleCookie = cookies.SimpleCookie()
214
+ cookie.load(header)
215
+ for key, morsel in cookie.items():
216
+ if morsel.value:
217
+ self.session.cookie_jar.update_cookies(cookie)
218
+
219
+ skey = self.session.cookie_jar.filter_cookies(self.auth_url).get("p_skey").value
220
+ async with self.session.post(
221
+ "https://graph.qq.com/oauth2.0/authorize",
222
+ params={
223
+ "Referer": "https://graph.qq.com/oauth2.0/show?which=Login&display=pc&response_type=code&client_id"
224
+ "=100497308&redirect_uri=https://y.qq.com/portal/wx_redirect.html?login_type=1&surl=https"
225
+ "://y.qq.com/portal/profile.html#stat=y_new.top.user_pic&stat=y_new.top.pop.logout"
226
+ "&use_customer_cb=0&state=state&display=pc",
227
+ "Content-Type": "application/x-www-form-urlencoded",
228
+ },
229
+ data={
230
+ "response_type": "code",
231
+ "client_id": "100497308",
232
+ "redirect_uri": "https://y.qq.com/portal/wx_redirect.html?login_type=1&surl=https://y.qq.com"
233
+ "/#&use_customer_cb=0",
234
+ "scope": "",
235
+ "state": "state",
236
+ "switch": "",
237
+ "from_ptlogin": "1",
238
+ "src": "1",
239
+ "update_auth": "1",
240
+ "openapi": "80901010",
241
+ "g_tk": hash33(skey, 5381),
242
+ "auth_time": str(int(time.time())),
243
+ "ui": random_uuid(),
244
+ },
245
+ allow_redirects=False,
246
+ ) as res:
247
+ location = res.headers.get("Location", "")
248
+ code = re.findall(r"(?<=code=)(.+?)(?=&)", location)[0]
249
+ res = (
250
+ await Api(**API["QQ_login"])
251
+ .update_params(code=code)
252
+ .update_extra_common(tmeLoginType="2")
253
+ .result
254
+ )
255
+ self.credential = Credential.from_cookies(res)
256
+ return self.credential
257
+
258
+
259
+ class WXLogin(Login):
260
+ """微信登录"""
261
+
262
+ def __init__(self):
263
+ super().__init__()
264
+ self.uuid = ""
265
+
266
+ async def send_authcode(self):
267
+ raise NotImplementedError("不支持")
268
+
269
+ async def get_qrcode(self):
270
+ if self.initialized():
271
+ return self.qrcode_data
272
+ async with self.session.get(
273
+ "https://open.weixin.qq.com/connect/qrconnect",
274
+ params={
275
+ "appid": "wx48db31d50e334801",
276
+ "redirect_uri": "https://y.qq.com/portal/wx_redirect.html?login_type=2&surl=https://y.qq.com/",
277
+ "response_type": "code",
278
+ "scope": "snsapi_login",
279
+ "state": "STATE",
280
+ "href": "https://y.qq.com/mediastyle/music_v17/src/css/popup_wechat.css#wechat_redirect",
281
+ },
282
+ ) as res:
283
+ self.uuid = re.findall(r"uuid=(.+?)\"", await res.text())[0]
284
+ async with self.session.get(
285
+ f"https://open.weixin.qq.com/connect/qrcode/{self.uuid}",
286
+ headers={
287
+ "referer": "https://open.weixin.qq.com/connect/qrconnect?appid=wx48db31d50e334801"
288
+ "&redirect_uri="
289
+ "https%3A%2F%2Fy.qq.com%2Fportal%2Fwx_redirect.html%3Flogin_type%3D2%26surl%3Dhttps"
290
+ "%3A%2F%2Fy.qq.com%2F"
291
+ "&response_type=code&scope=snsapi_login&state=STATE"
292
+ "&href=https%3A%2F%2Fy.qq.com%2Fmediastyle%2Fmusic_v17%2Fsrc%2Fcss%2Fpopup_wechat.css"
293
+ "%23wechat_redirect"
294
+ },
295
+ ) as res:
296
+ self.qrcode_data = await res.read()
297
+ return self.qrcode_data
298
+
299
+ async def get_qrcode_state(self):
300
+ async with self.session.get(
301
+ "https://lp.open.weixin.qq.com/connect/l/qrconnect",
302
+ headers={
303
+ "referer": "https://open.weixin.qq.com/",
304
+ },
305
+ params={
306
+ "uuid": self.uuid,
307
+ "_": str(int(round(time.time() * 1000))),
308
+ },
309
+ ) as res:
310
+ data = await res.text()
311
+
312
+ text_to_state = {
313
+ "408": QrCodeLoginEvents.SCAN,
314
+ "404": QrCodeLoginEvents.CONF,
315
+ "403": QrCodeLoginEvents.REFUSE,
316
+ "405": QrCodeLoginEvents.DONE,
317
+ }
318
+ state = QrCodeLoginEvents.OTHER
319
+ for text, value in text_to_state.items():
320
+ if text in await res.text():
321
+ state = value
322
+ break
323
+ self.state = state
324
+ if state == QrCodeLoginEvents.DONE:
325
+ self.musicid = re.findall(r"wx_code='(.+?)';", data)[0]
326
+ self.auth_url = (
327
+ "https://y.qq.com/portal/wx_redirect.html?login_type=2"
328
+ f"&surl=https://y.qq.com/&code={self.musicid}&state=STATE"
329
+ )
330
+ await self.session.get(
331
+ "https://lp.open.weixin.qq.com/connect/l/qrconnect",
332
+ params={
333
+ "uuid": self.uuid,
334
+ "_": str(int(round(time.time() * 1000))),
335
+ "last": "404",
336
+ },
337
+ )
338
+ return state
339
+
340
+ async def authorize(self, authcode: Optional[int] = None):
341
+ if self.credential:
342
+ return self.credential
343
+ await self.session.get(self.auth_url, allow_redirects=False)
344
+ res = (
345
+ await Api(**API["WX_login"])
346
+ .update_params(strAppid="wx48db31d50e334801", code=self.musicid)
347
+ .update_extra_common(tmeLoginType="1")
348
+ .result
349
+ )
350
+ self.credential = Credential.from_cookies(res)
351
+ return self.credential
352
+
353
+
354
+ class PhoneLogin(Login):
355
+ """手机号登录"""
356
+
357
+ def __init__(self, phone: int):
358
+ """
359
+ Args:
360
+ phone: 手机号码
361
+ """
362
+ pattern = re.compile(r"^1[3-9]\d{9}$")
363
+ if not pattern.match(str(phone)):
364
+ raise ValueError("非法手机号")
365
+ self.phone = phone
366
+
367
+ async def __aenter__(self):
368
+ pass
369
+
370
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
371
+ pass
372
+
373
+ async def close(self):
374
+ pass
375
+
376
+ async def get_qrcode(self):
377
+ raise NotImplementedError("不支持")
378
+
379
+ async def get_qrcode_state(self):
380
+ raise NotImplementedError("不支持")
381
+
382
+ async def send_authcode(self):
383
+ params = {
384
+ "tmeAppid": "qqmusic",
385
+ "phoneNo": str(self.phone),
386
+ "areaCode": "86",
387
+ }
388
+ msg = ""
389
+ res = await Api(**API["send_authcode"]).update_params(**params).result
390
+ msg = res["errMsg"]
391
+ if msg == "OK":
392
+ return PhoneLoginEvents.SEND
393
+ elif msg == "robot defense":
394
+ self.auth_url = res["securityURL"]
395
+ return PhoneLoginEvents.CAPTCHA
396
+ else:
397
+ return PhoneLoginEvents.OTHER
398
+
399
+ async def authorize(self, authcode: Optional[int] = None):
400
+ if not authcode:
401
+ raise ValueError("authcode 为空")
402
+ 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()
414
+
415
+
416
+ async def refresh_cookies(credential: Credential) -> Credential:
417
+ """
418
+ 刷新 Cookies
419
+
420
+ Args:
421
+ credential (Credential): 用户凭证
422
+
423
+ Return:
424
+ Credential: 新的用户凭证
425
+ """
426
+ params = {
427
+ "refresh_key": credential.refresh_key,
428
+ "musicid": credential.musicid,
429
+ "loginMode": 2,
430
+ }
431
+ res = (
432
+ await Api(**API["refresh"])
433
+ .update_params(**params)
434
+ .update_extra_common(tmeLoginType=str(credential.login_type))
435
+ .result
436
+ )
437
+ return Credential.from_cookies(res)
qqmusic_api/api/mv.py ADDED
@@ -0,0 +1,115 @@
1
+ import random
2
+
3
+ from ..utils.common import get_api, random_string
4
+ from ..utils.network import Api
5
+ from .song import query_by_id
6
+
7
+ API = get_api("mv")
8
+
9
+
10
+ class MV:
11
+ """
12
+ MV 类
13
+ """
14
+
15
+ def __init__(self, vid: str):
16
+ self.vid = vid
17
+
18
+ async def get_detail(self) -> dict:
19
+ """
20
+ 获取 MV 详细信息
21
+
22
+ Return:
23
+ dict: 视频信息
24
+ """
25
+ param = {
26
+ "vidlist": [self.vid],
27
+ "required": [
28
+ "vid",
29
+ "type",
30
+ "sid",
31
+ "cover_pic",
32
+ "duration",
33
+ "singers",
34
+ "video_switch",
35
+ "msg",
36
+ "name",
37
+ "desc",
38
+ "playcnt",
39
+ "pubdate",
40
+ "isfav",
41
+ "gmid",
42
+ "uploader_headurl",
43
+ "uploader_nick",
44
+ "uploader_encuin",
45
+ "uploader_uin",
46
+ "uploader_hasfollow",
47
+ "uploader_follower_num",
48
+ "uploader_hasfollow",
49
+ ],
50
+ }
51
+ return (await Api(**API["detail"]).update_params(**param).result)[self.vid]
52
+
53
+ async def get_related_song(self) -> list[dict]:
54
+ """
55
+ 获取 MV 相关歌曲
56
+
57
+ Return:
58
+ list: 歌曲基本信息
59
+ """
60
+ param = {
61
+ "vidlist": [self.vid],
62
+ "required": ["related_songs"],
63
+ }
64
+ song_id = (await Api(**API["detail"]).update_params(**param).result)[self.vid][
65
+ "related_songs"
66
+ ]
67
+ return await query_by_id(song_id)
68
+
69
+ async def get_url(self) -> dict:
70
+ """
71
+ 获取 MV 播放链接
72
+
73
+ Return:
74
+ dict: 视频播放链接
75
+ """
76
+ return (await get_mv_urls([self.vid]))[self.vid]
77
+
78
+
79
+ async def get_mv_urls(vid: list[str]) -> dict:
80
+ """
81
+ 获取 MV 播放链接
82
+
83
+ Args:
84
+ vid: 视频 vid 列表
85
+
86
+ Return:
87
+ dict: 视频播放链接
88
+ """
89
+ param = {
90
+ "vids": vid,
91
+ "request_type": 10003,
92
+ "guid": random_string(32, "abcdef1234567890"),
93
+ "videoformat": 1,
94
+ "format": 265,
95
+ "dolby": 1,
96
+ "use_new_domain": 1,
97
+ "use_ipv6": 1,
98
+ }
99
+ result = await Api(**API["url"]).update_params(**param).result
100
+ urls: dict[str, dict] = {}
101
+
102
+ def get_play_urls(data):
103
+ play_urls = {}
104
+ for url_info in data:
105
+ if url_info["freeflow_url"]:
106
+ play_url = random.choice(url_info["freeflow_url"])
107
+ play_urls[url_info["filetype"]] = play_url
108
+ return play_urls
109
+
110
+ for vid, data in result.items():
111
+ urls[vid] = {}
112
+ urls[vid]["mp4"] = get_play_urls(data["mp4"])
113
+ urls[vid]["hls"] = get_play_urls(data["hls"])
114
+
115
+ return urls