qqmusic-api-python 0.7.0__py3-none-any.whl → 0.7.2__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 +1 -1
- qqmusic_api/core/__init__.py +6 -2
- qqmusic_api/core/api_context.py +227 -0
- qqmusic_api/core/client.py +253 -386
- qqmusic_api/core/pagination.py +525 -344
- qqmusic_api/core/request.py +286 -65
- qqmusic_api/models/comment.py +18 -0
- qqmusic_api/models/lyric.py +112 -10
- qqmusic_api/models/request.py +0 -30
- qqmusic_api/models/search.py +193 -0
- qqmusic_api/models/singer.py +28 -1
- qqmusic_api/modules/_base.py +162 -282
- qqmusic_api/modules/album.py +18 -14
- qqmusic_api/modules/comment.py +108 -69
- qqmusic_api/modules/helper.py +2 -8
- qqmusic_api/modules/login.py +38 -32
- qqmusic_api/modules/lyric.py +85 -8
- qqmusic_api/modules/mv.py +10 -8
- qqmusic_api/modules/private_message.py +35 -36
- qqmusic_api/modules/recommend.py +28 -42
- qqmusic_api/modules/search.py +156 -38
- qqmusic_api/modules/singer.py +68 -46
- qqmusic_api/modules/song.py +38 -33
- qqmusic_api/modules/songlist.py +13 -14
- qqmusic_api/modules/top.py +9 -10
- qqmusic_api/modules/user.py +85 -88
- qqmusic_api/utils/mqtt.py +4 -2
- {qqmusic_api_python-0.7.0.dist-info → qqmusic_api_python-0.7.2.dist-info}/METADATA +1 -2
- qqmusic_api_python-0.7.2.dist-info/RECORD +56 -0
- qqmusic_api_python-0.7.0.dist-info/RECORD +0 -55
- {qqmusic_api_python-0.7.0.dist-info → qqmusic_api_python-0.7.2.dist-info}/WHEEL +0 -0
- {qqmusic_api_python-0.7.0.dist-info → qqmusic_api_python-0.7.2.dist-info}/licenses/LICENSE +0 -0
qqmusic_api/__init__.py
CHANGED
qqmusic_api/core/__init__.py
CHANGED
|
@@ -19,7 +19,7 @@ from .exceptions import (
|
|
|
19
19
|
NetworkError,
|
|
20
20
|
RatelimitedError,
|
|
21
21
|
)
|
|
22
|
-
from .request import
|
|
22
|
+
from .request import BaseRequest, CgiRequest, HttpRequest, ItemPaginatedCgiRequest, PaginatedCgiRequest
|
|
23
23
|
from .versioning import DEFAULT_VERSION_POLICY, Platform, VersionPolicy, VersionProfile
|
|
24
24
|
|
|
25
25
|
__all__ = [
|
|
@@ -27,22 +27,26 @@ __all__ = [
|
|
|
27
27
|
"ApiDataError",
|
|
28
28
|
"ApiException",
|
|
29
29
|
"BaseApiException",
|
|
30
|
+
"BaseRequest",
|
|
30
31
|
"CgiApiException",
|
|
32
|
+
"CgiRequest",
|
|
31
33
|
"Client",
|
|
32
34
|
"CredentialExpiredError",
|
|
33
35
|
"CredentialInvalidError",
|
|
34
36
|
"CredentialRefreshError",
|
|
35
37
|
"GlobalApiError",
|
|
36
38
|
"HTTPError",
|
|
39
|
+
"HttpRequest",
|
|
40
|
+
"ItemPaginatedCgiRequest",
|
|
37
41
|
"LoginAccountRestrictedError",
|
|
38
42
|
"LoginAuthExpiredError",
|
|
39
43
|
"LoginDeviceLimitError",
|
|
40
44
|
"LoginError",
|
|
41
45
|
"LoginRateLimitError",
|
|
42
46
|
"NetworkError",
|
|
47
|
+
"PaginatedCgiRequest",
|
|
43
48
|
"Platform",
|
|
44
49
|
"RatelimitedError",
|
|
45
|
-
"Request",
|
|
46
50
|
"VersionPolicy",
|
|
47
51
|
"VersionProfile",
|
|
48
52
|
]
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"""API Context 管理类. 维护全局请求环境、会话与参数构造逻辑."""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from collections.abc import Sequence
|
|
5
|
+
from typing import Any, cast
|
|
6
|
+
|
|
7
|
+
import anyio
|
|
8
|
+
import orjson as json
|
|
9
|
+
from niquests import AsyncSession
|
|
10
|
+
from niquests.exceptions import RequestException
|
|
11
|
+
|
|
12
|
+
from ..algorithms import zzc_sign
|
|
13
|
+
from ..models.request import Credential
|
|
14
|
+
from ..utils.device import Device, DeviceManager
|
|
15
|
+
from ..utils.qimei import QimeiManager
|
|
16
|
+
from .exceptions import HTTPError, NetworkError
|
|
17
|
+
from .versioning import DEFAULT_VERSION_POLICY, Platform, VersionPolicy
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ApiContext:
|
|
21
|
+
"""全局请求上下文与会话管理."""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
credential: Credential | None = None,
|
|
26
|
+
*,
|
|
27
|
+
platform: Platform | None = None,
|
|
28
|
+
device_path: str | None = None,
|
|
29
|
+
version_policy: VersionPolicy = DEFAULT_VERSION_POLICY,
|
|
30
|
+
session: AsyncSession,
|
|
31
|
+
) -> None:
|
|
32
|
+
"""初始化 ApiContext.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
credential: 全局默认凭证.
|
|
36
|
+
platform: 全局默认请求平台.
|
|
37
|
+
device_path: 设备信息文件路径.
|
|
38
|
+
version_policy: 版本策略规则.
|
|
39
|
+
session: HTTP 异步 Session.
|
|
40
|
+
"""
|
|
41
|
+
self.credential = credential or Credential()
|
|
42
|
+
self.platform = platform or Platform.ANDROID
|
|
43
|
+
self.version_policy = version_policy
|
|
44
|
+
self._session = session
|
|
45
|
+
self._device_store = DeviceManager(device_path)
|
|
46
|
+
self._session_lock = anyio.Lock()
|
|
47
|
+
self._qimei_manager = QimeiManager(
|
|
48
|
+
device_store=self._device_store,
|
|
49
|
+
app_version=self.version_policy.get_qimei_app_version(),
|
|
50
|
+
sdk_version=self.version_policy.get_qimei_sdk_version(),
|
|
51
|
+
session=self._session,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
async def get_user_agent(self, platform: Platform | None = None) -> str:
|
|
55
|
+
"""根据指定或默认平台生成请求所需的 User-Agent.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
platform: 平台标识. 若为 None, 使用当前上下文默认平台.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
格式化好的 User-Agent 字符串.
|
|
62
|
+
"""
|
|
63
|
+
target_platform = platform or self.platform
|
|
64
|
+
return self.version_policy.get_user_agent(target_platform, await self._device_store.get_device())
|
|
65
|
+
|
|
66
|
+
async def ensure_session(self, platform: Platform | None = None) -> None:
|
|
67
|
+
"""校验并更新平台会话信息 (针对 Android 平台)."""
|
|
68
|
+
target_platform = platform or self.platform
|
|
69
|
+
if target_platform != Platform.ANDROID:
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
def _is_session_valid(dev: Device) -> bool:
|
|
73
|
+
return (
|
|
74
|
+
dev.session_save_time is not None
|
|
75
|
+
and (int(time.time()) - dev.session_save_time) < 86400
|
|
76
|
+
and bool(dev.session_uid and dev.session_sid)
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
device = await self._device_store.get_device()
|
|
80
|
+
if _is_session_valid(device):
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
async with self._session_lock:
|
|
84
|
+
device = await self._device_store.get_device()
|
|
85
|
+
if _is_session_valid(device):
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
finalcomm = self.version_policy.build_comm(
|
|
89
|
+
platform=Platform.ANDROID,
|
|
90
|
+
credential=self.credential,
|
|
91
|
+
device=device,
|
|
92
|
+
qimei=cast("dict[str, str]", await self._qimei_manager.get_cached()),
|
|
93
|
+
guid=device.open_udid,
|
|
94
|
+
)
|
|
95
|
+
payload: dict[str, Any] = {
|
|
96
|
+
"comm": finalcomm,
|
|
97
|
+
"req_0": {
|
|
98
|
+
"module": "music.getSession.session",
|
|
99
|
+
"method": "GetSession",
|
|
100
|
+
"param": {
|
|
101
|
+
"uid": device.session_uid or "",
|
|
102
|
+
"vkey": 0,
|
|
103
|
+
"caller": 0,
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
}
|
|
107
|
+
user_agent = await self.get_user_agent(Platform.ANDROID)
|
|
108
|
+
try:
|
|
109
|
+
resp = await self._session.post(
|
|
110
|
+
"https://u.y.qq.com/cgi-bin/musicu.fcg",
|
|
111
|
+
json=payload,
|
|
112
|
+
headers={"User-Agent": user_agent},
|
|
113
|
+
)
|
|
114
|
+
await self._session.gather(resp)
|
|
115
|
+
except RequestException as exc:
|
|
116
|
+
raise NetworkError(str(exc)) from exc
|
|
117
|
+
|
|
118
|
+
if resp.status_code != 200:
|
|
119
|
+
raise HTTPError(
|
|
120
|
+
f"HTTP 请求状态码异常: {resp.status_code}",
|
|
121
|
+
status_code=cast("int", resp.status_code),
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
resp_data = resp.json()
|
|
125
|
+
session_data = resp_data["req_0"]["data"]["session"]
|
|
126
|
+
device.session_uid = str(session_data["uid"])
|
|
127
|
+
device.session_sid = session_data["sid"]
|
|
128
|
+
device.session_vkey = session_data.get("vkey")
|
|
129
|
+
device.session_save_time = int(time.time())
|
|
130
|
+
await self._device_store.save_device()
|
|
131
|
+
|
|
132
|
+
async def prepare_http_kwargs(
|
|
133
|
+
self,
|
|
134
|
+
credential: Credential | None = None,
|
|
135
|
+
**kwargs: Any,
|
|
136
|
+
) -> dict[str, Any]:
|
|
137
|
+
"""准备标准 HTTP 请求所需要的 kwargs (注入 Cookies 和 User-Agent).
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
credential: 请求凭证, 优先于上下文默认凭证.
|
|
141
|
+
platform: 请求平台, 优先于上下文默认平台.
|
|
142
|
+
**kwargs: 额外请求参数.
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
组装好的 kwargs 字典.
|
|
146
|
+
"""
|
|
147
|
+
prepared = kwargs.copy()
|
|
148
|
+
cred = credential or self.credential
|
|
149
|
+
user_cookies = prepared.pop("cookies", {})
|
|
150
|
+
cookies: dict[str, str] = {}
|
|
151
|
+
if cred.musicid:
|
|
152
|
+
cookies["uin"] = cred.str_musicid or str(cred.musicid)
|
|
153
|
+
cookies["qqmusic_uin"] = cred.str_musicid or str(cred.musicid)
|
|
154
|
+
if cred.musickey:
|
|
155
|
+
cookies["qm_keyst"] = cred.musickey
|
|
156
|
+
cookies["qqmusic_key"] = cred.musickey
|
|
157
|
+
cookies.update(user_cookies)
|
|
158
|
+
if cookies:
|
|
159
|
+
prepared["cookies"] = cookies
|
|
160
|
+
|
|
161
|
+
headers = prepared.get("headers", {})
|
|
162
|
+
if "User-Agent" not in headers:
|
|
163
|
+
headers["User-Agent"] = await self.get_user_agent(Platform.WEB)
|
|
164
|
+
prepared["headers"] = headers
|
|
165
|
+
|
|
166
|
+
return prepared
|
|
167
|
+
|
|
168
|
+
async def build_api_kwargs(
|
|
169
|
+
self,
|
|
170
|
+
data: Sequence[dict[str, Any]],
|
|
171
|
+
comm: dict[str, Any] | None = None,
|
|
172
|
+
credential: Credential | None = None,
|
|
173
|
+
platform: Platform | None = None,
|
|
174
|
+
*,
|
|
175
|
+
override_comm: bool = False,
|
|
176
|
+
sign: bool = False,
|
|
177
|
+
) -> tuple[str, dict[str, Any], dict[str, str], dict[str, str]]:
|
|
178
|
+
"""构建 CGI 接口调用的 URL、payload、params 与 headers.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
data: 请求子项列表.
|
|
182
|
+
comm: 自定义公共参数.
|
|
183
|
+
credential: 请求凭证.
|
|
184
|
+
platform: 请求平台.
|
|
185
|
+
override_comm: 是否完全覆盖默认公共参数.
|
|
186
|
+
sign: 是否需要签名 URL.
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
(url, payload, params, headers) 四元组.
|
|
190
|
+
"""
|
|
191
|
+
target_platform = platform or self.platform
|
|
192
|
+
if target_platform == Platform.ANDROID:
|
|
193
|
+
await self.ensure_session(target_platform)
|
|
194
|
+
|
|
195
|
+
device = await self._device_store.get_device()
|
|
196
|
+
if override_comm:
|
|
197
|
+
final = (comm or {}).copy()
|
|
198
|
+
else:
|
|
199
|
+
final = self.version_policy.build_comm(
|
|
200
|
+
platform=target_platform,
|
|
201
|
+
credential=credential or self.credential,
|
|
202
|
+
device=device,
|
|
203
|
+
qimei=cast("dict[str, str]", await self._qimei_manager.get_cached())
|
|
204
|
+
if target_platform == Platform.ANDROID
|
|
205
|
+
else None,
|
|
206
|
+
guid=device.open_udid,
|
|
207
|
+
)
|
|
208
|
+
if comm:
|
|
209
|
+
final.update(comm)
|
|
210
|
+
|
|
211
|
+
user_agent = await self.get_user_agent(target_platform)
|
|
212
|
+
|
|
213
|
+
payload: dict[str, Any] = {
|
|
214
|
+
"comm": final,
|
|
215
|
+
}
|
|
216
|
+
params: dict[str, str] = {}
|
|
217
|
+
for idx, req in enumerate(data):
|
|
218
|
+
payload[f"req_{idx}"] = req
|
|
219
|
+
|
|
220
|
+
if sign:
|
|
221
|
+
params["_"] = str(int(time.time() * 1000))
|
|
222
|
+
params["sign"] = zzc_sign(json.dumps(payload))
|
|
223
|
+
|
|
224
|
+
url = "https://u.y.qq.com/cgi-bin/musicu.fcg" if not sign else "https://u.y.qq.com/cgi-bin/musics.fcg"
|
|
225
|
+
headers = {"User-Agent": user_agent}
|
|
226
|
+
|
|
227
|
+
return url, payload, params, headers
|