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.
- qqmusic_api/__init__.py +4 -0
- qqmusic_api/api/album.py +40 -0
- qqmusic_api/api/login.py +437 -0
- qqmusic_api/api/mv.py +115 -0
- qqmusic_api/api/search.py +168 -0
- qqmusic_api/api/song.py +389 -0
- qqmusic_api/api/songlist.py +81 -0
- qqmusic_api/api/top.py +98 -0
- qqmusic_api/data/api/album.json +20 -0
- qqmusic_api/data/api/login.json +69 -0
- qqmusic_api/data/api/mv.json +26 -0
- qqmusic_api/data/api/search.json +73 -0
- qqmusic_api/data/api/singer.json +1 -0
- qqmusic_api/data/api/song.json +105 -0
- qqmusic_api/data/api/songlist.json +17 -0
- qqmusic_api/data/api/top.json +20 -0
- qqmusic_api/data/file_type.json +50 -0
- qqmusic_api/data/search_type.json +11 -0
- qqmusic_api/exceptions.py +121 -0
- qqmusic_api/settings.py +22 -0
- qqmusic_api/utils/__init__.py +0 -0
- qqmusic_api/utils/common.py +140 -0
- qqmusic_api/utils/credential.py +92 -0
- qqmusic_api/utils/network.py +249 -0
- qqmusic_api/utils/qimei.py +267 -0
- qqmusic_api_python-0.1.0.dist-info/LICENSE +21 -0
- qqmusic_api_python-0.1.0.dist-info/METADATA +99 -0
- qqmusic_api_python-0.1.0.dist-info/RECORD +29 -0
- qqmusic_api_python-0.1.0.dist-info/WHEEL +4 -0
qqmusic_api/__init__.py
ADDED
qqmusic_api/api/album.py
ADDED
|
@@ -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"]]
|
qqmusic_api/api/login.py
ADDED
|
@@ -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
|