qqmusic-api-python 0.3.0__py3-none-any.whl → 0.3.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 CHANGED
@@ -4,7 +4,7 @@ from . import album, login, lyric, mv, search, singer, song, songlist, top, user
4
4
  from .utils.credential import Credential
5
5
  from .utils.session import Session, get_session, set_session
6
6
 
7
- __version__ = "0.3.0"
7
+ __version__ = "0.3.2"
8
8
 
9
9
  logger = logging.getLogger("qqmusicapi")
10
10
 
qqmusic_api/login.py CHANGED
@@ -103,8 +103,6 @@ class QRCodeLoginEvents(Enum):
103
103
  return member
104
104
  return cls.OTHER
105
105
 
106
- ""
107
-
108
106
 
109
107
  class PhoneLoginEvents(Enum):
110
108
  """手机登录状态
qqmusic_api/singer.py CHANGED
@@ -3,7 +3,7 @@
3
3
  from enum import Enum
4
4
  from typing import Any, Literal, cast
5
5
 
6
- from .utils.network import api_request
6
+ from .utils.network import RequestGroup, api_request
7
7
 
8
8
 
9
9
  class AreaType(Enum):
@@ -63,21 +63,46 @@ class TabType(Enum):
63
63
  self.tab_name = tab_name
64
64
 
65
65
 
66
- # 动态生成 A-Z 的枚举值
67
- letters = {chr(i): idx for idx, i in enumerate(range(ord('A'), ord('Z') + 1), start=1)}
68
- letters.update({"ALL": -100, "HASH": 27})
69
- # 创建枚举类 IndexType
70
- IndexType = Enum('IndexType', letters)
66
+ class IndexType(Enum):
67
+ """首字母索引枚举"""
68
+
69
+ A = 1
70
+ B = 2
71
+ C = 3
72
+ D = 4
73
+ E = 5
74
+ F = 6
75
+ G = 7
76
+ H = 8
77
+ I = 9 # noqa: E741
78
+ J = 10
79
+ K = 11
80
+ L = 12
81
+ M = 13
82
+ N = 14
83
+ O = 15 # noqa: E741
84
+ P = 16
85
+ Q = 17
86
+ R = 18
87
+ S = 19
88
+ T = 20
89
+ U = 21
90
+ V = 22
91
+ W = 23
92
+ X = 24
93
+ Y = 25
94
+ Z = 26
95
+ ALL = -100
96
+ HASH = 27
71
97
 
72
98
 
73
99
  def validate_int_enum(value: int | Enum, enum_type: type[Enum]) -> int:
74
100
  """确保传入的值符合指定的枚举类型"""
75
101
  if isinstance(value, enum_type):
76
- return value.value # 如果是枚举成员,返回对应的整数值
77
- elif value in {item.value for item in enum_type}:
78
- return value # 如果是合法整数值,直接返回
79
- else:
80
- raise ValueError(f"Invalid value: {value} for {enum_type.__name__}")
102
+ return value.value # 如果是枚举成员,返回对应的整数值
103
+ if value in {item.value for item in enum_type}:
104
+ return cast(int, value) # 如果是合法整数值,直接返回
105
+ raise ValueError(f"Invalid value: {value} for {enum_type.__name__}")
81
106
 
82
107
 
83
108
  @api_request("music.musichallSinger.SingerList", "GetSingerList")
@@ -93,7 +118,6 @@ async def get_singer_list(
93
118
  sex: 性别
94
119
  genre: 风格
95
120
  """
96
-
97
121
  area = validate_int_enum(area, AreaType)
98
122
  sex = validate_int_enum(sex, SexType)
99
123
  genre = validate_int_enum(genre, GenreType)
@@ -110,7 +134,7 @@ async def get_singer_list(
110
134
 
111
135
 
112
136
  @api_request("music.musichallSinger.SingerList", "GetSingerListIndex")
113
- async def get_singer_list_index_raw(
137
+ async def get_singer_list_index(
114
138
  area: int | AreaType = AreaType.ALL,
115
139
  sex: int | SexType = SexType.ALL,
116
140
  genre: int | GenreType = GenreType.ALL,
@@ -128,7 +152,6 @@ async def get_singer_list_index_raw(
128
152
  sin: 跳过数量
129
153
  cur_page: 当前页
130
154
  """
131
-
132
155
  area = validate_int_enum(area, AreaType)
133
156
  sex = validate_int_enum(sex, SexType)
134
157
  genre = validate_int_enum(genre, GenreType)
@@ -147,29 +170,6 @@ async def get_singer_list_index_raw(
147
170
  )
148
171
 
149
172
 
150
- async def get_singer_list_index(
151
- area: int | AreaType = AreaType.ALL,
152
- sex: int | SexType = SexType.ALL,
153
- genre: int | GenreType = GenreType.ALL,
154
- index: int | IndexType = IndexType.ALL,
155
- sin: int = 0,
156
- cur_page: int = 1,
157
- ):
158
- """获取自定义页歌手列表
159
- """
160
-
161
- area = validate_int_enum(area, AreaType)
162
- sex = validate_int_enum(sex, SexType)
163
- genre = validate_int_enum(genre, GenreType)
164
- index = validate_int_enum(index, IndexType)
165
-
166
- data = await get_singer_list_index_raw(
167
- area = area, sex = sex, genre = genre, index = index, sin = sin, cur_page = cur_page
168
- )
169
-
170
- return cast(list[dict[str, Any]], data["singerlist"])
171
-
172
-
173
173
  async def get_singer_list_index_all(
174
174
  area: int | AreaType = AreaType.ALL,
175
175
  sex: int | SexType = SexType.ALL,
@@ -177,32 +177,35 @@ async def get_singer_list_index_all(
177
177
  index: int | IndexType = IndexType.ALL,
178
178
  ):
179
179
  """获取所有歌手列表
180
- """
181
180
 
181
+ Args:
182
+ area: 地区
183
+ sex: 性别
184
+ genre: 风格
185
+ index: 索引
186
+ """
182
187
  area = validate_int_enum(area, AreaType)
183
188
  sex = validate_int_enum(sex, SexType)
184
189
  genre = validate_int_enum(genre, GenreType)
185
190
  index = validate_int_enum(index, IndexType)
186
191
 
187
- data = await get_singer_list_index_raw(
188
- area = area, sex = sex, genre = genre, index = index, sin = 0, cur_page = 1
189
- )
192
+ data = await get_singer_list_index(area=area, sex=sex, genre=genre, index=index, sin=0, cur_page=1)
190
193
 
191
194
  singer_list = data["singerlist"]
192
195
  total = data["total"]
193
196
  if total <= 80:
194
197
  return cast(list[dict[str, Any]], singer_list)
195
198
 
196
- # 每页80个歌手,向下取整
199
+ # 每页80个歌手,向下取整
197
200
  pages = total // 80
198
201
  sin = 80
202
+ rg = RequestGroup()
199
203
  for page in range(2, pages + 2):
200
- data = await get_singer_list_index_raw(
201
- area = area, sex = sex, genre = genre, index = index, sin = sin, cur_page = page
202
- )
203
- singer_list.extend(data["singerlist"])
204
+ rg.add_request(get_singer_list_index, area=area, sex=sex, genre=genre, index=index, sin=sin, cur_page=page)
204
205
  sin += 80
205
206
 
207
+ for data in await rg.execute():
208
+ singer_list.extend(data["singerlist"])
206
209
  return cast(list[dict[str, Any]], singer_list)
207
210
 
208
211
 
@@ -252,6 +255,17 @@ async def get_desc(mids: list[str]):
252
255
  return {"singer_mids": mids, "groups": 1, "wikis": 1}, lambda data: cast(list[dict[str, Any]], data["singer_list"])
253
256
 
254
257
 
258
+ @api_request("music.SimilarSingerSvr", "GetSimilarSingerList")
259
+ async def get_similar(mid: str, number: int = 10):
260
+ """获取类似歌手列表
261
+
262
+ Args:
263
+ mid: 歌手 mid
264
+ number: 类似歌手数量
265
+ """
266
+ return {"singerMid": mid, "number": number}, lambda data: cast(list[dict[str, Any]], data["singerlist"])
267
+
268
+
255
269
  async def get_songs(
256
270
  mid: str,
257
271
  tab_type: Literal[
@@ -275,3 +289,135 @@ async def get_songs(
275
289
  num: 返回数量
276
290
  """
277
291
  return await get_tab_detail(mid, tab_type, page, num)
292
+
293
+
294
+ @api_request("musichall.song_list_server", "GetSingerSongList")
295
+ async def get_songs_list(mid: str, number: int = 10, begin: int = 0):
296
+ """获取歌手歌曲原始数据
297
+
298
+ Args:
299
+ mid: 歌手 mid
300
+ number: 每次获取数量,最大30
301
+ begin: 从第几个开始
302
+ """
303
+ return {
304
+ "singerMid": mid,
305
+ "order": 1,
306
+ "number": number,
307
+ "begin": begin,
308
+ }, lambda data: cast(
309
+ dict[str, Any],
310
+ data,
311
+ )
312
+
313
+
314
+ async def get_songs_list_all(mid: str):
315
+ """获取歌手所有歌曲列表
316
+
317
+ Args:
318
+ mid: 歌手 mid
319
+ """
320
+ response = await get_songs_list(mid=mid, number=30, begin=0)
321
+
322
+ total = response["totalNum"]
323
+ songs = [song["songInfo"] for song in response["songList"]]
324
+ if total <= 30:
325
+ return cast(list[dict[str, Any]], songs)
326
+
327
+ rg = RequestGroup()
328
+ for num in range(30, total, 30):
329
+ rg.add_request(get_songs_list, mid=mid, number=30, begin=num)
330
+
331
+ response = await rg.execute()
332
+ for res in response:
333
+ songs.extend([song["songInfo"] for song in res["songList"]])
334
+
335
+ return cast(list[dict[str, Any]], songs)
336
+
337
+
338
+ @api_request("music.musichallAlbum.AlbumListServer", "GetAlbumList")
339
+ async def get_album_list(mid: str, number: int = 10, begin: int = 0):
340
+ """获取歌手专辑
341
+
342
+ Args:
343
+ mid: 歌手 mid
344
+ number: 每次获取数量,不足30个的时候直接全部返回
345
+ begin: 从第几个开始
346
+ """
347
+ return {
348
+ "singerMid": mid,
349
+ "order": 1,
350
+ "number": number,
351
+ "begin": begin,
352
+ }, lambda data: cast(
353
+ dict[str, Any],
354
+ data,
355
+ )
356
+
357
+
358
+ async def get_album_list_all(mid: str):
359
+ """获取歌手所有专辑列表
360
+
361
+ Args:
362
+ mid: 歌手 mid
363
+ """
364
+ response = await get_album_list(mid=mid, number=30, begin=0)
365
+
366
+ total = response["total"]
367
+ albums = response["albumList"]
368
+ if total <= 30:
369
+ return cast(list[dict[str, Any]], albums)
370
+
371
+ rg = RequestGroup()
372
+ for num in range(30, total, 30):
373
+ rg.add_request(get_album_list, mid=mid, number=30, begin=num)
374
+
375
+ response = await rg.execute()
376
+ for res in response:
377
+ albums.extend(res["albumList"])
378
+
379
+ return cast(list[dict[str, Any]], albums)
380
+
381
+
382
+ @api_request("MvService.MvInfoProServer", "GetSingerMvList")
383
+ async def get_mv_list(mid: str, number: int = 10, begin: int = 0):
384
+ """获取歌手mv原始数据
385
+
386
+ Args:
387
+ mid: 歌手 mid
388
+ number: 每次获取数量,每次最大100
389
+ begin: 从第几个开始
390
+ """
391
+ return {
392
+ "singermid": mid,
393
+ "order": 1,
394
+ "count": number,
395
+ "start": begin,
396
+ }, lambda data: cast(
397
+ dict[str, Any],
398
+ data,
399
+ )
400
+
401
+
402
+ async def get_mv_list_all(mid: str):
403
+ """获取歌手所有专辑列表
404
+
405
+ Args:
406
+ mid: 歌手 mid
407
+ """
408
+ response = await get_mv_list(mid=mid, number=100, begin=0)
409
+
410
+ total = response["total"]
411
+ mvs = response["list"]
412
+ if total <= 100:
413
+ return cast(list[dict[str, Any]], mvs)
414
+
415
+ rg = RequestGroup()
416
+ for num in range(100, total, 100):
417
+ rg.add_request(get_mv_list, mid=mid, number=100, begin=num)
418
+
419
+ response = await rg.execute()
420
+ for res in response:
421
+ mvs.extend(res["list"])
422
+
423
+ return cast(list[dict[str, Any]], mvs)
qqmusic_api/song.py CHANGED
@@ -179,7 +179,6 @@ async def get_song_urls(
179
179
  params=params,
180
180
  credential=credential,
181
181
  exclude_params=["guid"],
182
- cacheable=False,
183
182
  )
184
183
  req.processor = _processor
185
184
  rg.add_request(req)
@@ -1,10 +1,12 @@
1
1
  """凭据类,用于请求验证"""
2
2
 
3
- import json
4
3
  import sys
5
4
  from dataclasses import asdict, dataclass, field
5
+ from time import time
6
6
  from typing import Any
7
7
 
8
+ import orjson as json
9
+
8
10
  if sys.version_info >= (3, 11):
9
11
  from typing import Self
10
12
  else:
@@ -87,6 +89,12 @@ class Credential:
87
89
 
88
90
  async def is_expired(self) -> bool:
89
91
  """判断 credential 是否过期"""
92
+ if "musickeyCreateTime" in self.extra_fields and "keyExpiresIn" in self.extra_fields:
93
+ expired_time_stamp = self.extra_fields["musickeyCreateTime"] + self.extra_fields["keyExpiresIn"]
94
+ if expired_time_stamp >= time():
95
+ return True
96
+ return False
97
+
90
98
  from ..login import check_expired
91
99
 
92
100
  return await check_expired(self)
@@ -102,7 +110,7 @@ class Credential:
102
110
  """获取凭据 JSON 字符串"""
103
111
  data = self.as_dict()
104
112
  data.update(data.pop("extra_fields"))
105
- return json.dumps(data, indent=4, ensure_ascii=False)
113
+ return json.dumps(data).decode()
106
114
 
107
115
  @classmethod
108
116
  def from_cookies_dict(cls, cookies: dict[str, Any]) -> Self:
@@ -112,7 +120,7 @@ class Credential:
112
120
  refresh_token=cookies.pop("refresh_token", ""),
113
121
  access_token=cookies.pop("access_token", ""),
114
122
  expired_at=cookies.pop("expired_at", 0),
115
- musicid=cookies.pop("musicid", 0),
123
+ musicid=int(cookies.pop("musicid", 0)),
116
124
  musickey=cookies.pop("musickey", ""),
117
125
  unionid=cookies.pop("unionid", ""),
118
126
  str_musicid=cookies.pop(
@@ -2,7 +2,6 @@
2
2
 
3
3
  import binascii
4
4
  import hashlib
5
- import json
6
5
  import random
7
6
  import string
8
7
  from dataclasses import asdict, dataclass, field
@@ -10,6 +9,8 @@ from pathlib import Path
10
9
  from typing import ClassVar
11
10
  from uuid import uuid4
12
11
 
12
+ import orjson as json
13
+
13
14
  device_path = Path(__file__).parent.parent / ".cache" / "device.json"
14
15
 
15
16
 
@@ -97,4 +98,4 @@ def get_cached_device() -> Device:
97
98
  def save_device(device: Device):
98
99
  """缓存 Device"""
99
100
  device_path.parent.mkdir(parents=True, exist_ok=True)
100
- device_path.write_text(json.dumps(asdict(device)))
101
+ device_path.write_text(json.dumps(asdict(device)).decode())
@@ -1,6 +1,5 @@
1
1
  """网络请求"""
2
2
 
3
- import json
4
3
  import logging
5
4
  from abc import ABC, abstractmethod
6
5
  from collections import defaultdict
@@ -8,6 +7,7 @@ from collections.abc import Callable, Coroutine
8
7
  from typing import Any, ClassVar, Generic, ParamSpec, TypedDict, TypeVar, cast
9
8
 
10
9
  import httpx
10
+ import orjson as json
11
11
  from typing_extensions import override
12
12
 
13
13
  from ..exceptions import CredentialExpiredError, ResponseCodeError, SignInvalidError
@@ -110,7 +110,7 @@ class BaseRequest(ABC):
110
110
  self._common = value
111
111
 
112
112
  def _build_common_params(self, credential: Credential) -> dict[str, Any]:
113
- config = (self.session).api_config
113
+ config = self.session.api_config
114
114
  common = {
115
115
  "cv": config["version_code"],
116
116
  "v": config["version_code"],
@@ -223,8 +223,7 @@ class ApiRequest(BaseRequest, Generic[_P, _R]):
223
223
 
224
224
  @override
225
225
  def build_request_data(self) -> dict[str, Any]:
226
- common = self._build_common_params(self.credential)
227
- return {"comm": common, f"{self.module}.{self.method}": self.data}
226
+ return {"comm": self.common, f"{self.module}.{self.method}": self.data}
228
227
 
229
228
  @property
230
229
  def data(self) -> dict[str, Any]:
@@ -246,7 +245,7 @@ class ApiRequest(BaseRequest, Generic[_P, _R]):
246
245
  params.pop(key, None)
247
246
  if self.credential:
248
247
  params["credential"] = f"{self.credential.musicid}{self.credential.musickey}"
249
- sorted_params = json.dumps(params, sort_keys=True, ensure_ascii=False)
248
+ sorted_params = json.dumps(params, option=json.OPT_SORT_KEYS)
250
249
  return calc_md5(sorted_params)
251
250
 
252
251
  @override
@@ -255,7 +254,7 @@ class ApiRequest(BaseRequest, Generic[_P, _R]):
255
254
  if not resp.content:
256
255
  return {}
257
256
  try:
258
- data = resp.json()
257
+ data = json.loads(resp.content)
259
258
  except json.JSONDecodeError:
260
259
  return {"data": resp.text}
261
260
  req_data = data.get(f"{self.module}.{self.method}", {})
@@ -327,9 +326,11 @@ class RequestGroup(BaseRequest):
327
326
  self,
328
327
  common: dict[str, Any] | None = None,
329
328
  credential: Credential | None = None,
329
+ limit: int = 30,
330
330
  ):
331
331
  super().__init__(common, credential)
332
332
  self._requests: list[RequestItem] = []
333
+ self.limit = limit
333
334
  self._key_counter = defaultdict(int)
334
335
  self._results = []
335
336
 
@@ -358,11 +359,11 @@ class RequestGroup(BaseRequest):
358
359
  if not resp.content:
359
360
  return []
360
361
 
361
- data = resp.json()
362
+ res_data = json.loads(resp.content)
362
363
 
363
364
  for req_item in self._requests:
364
365
  req = req_item["request"]
365
- req_data = data.get(req_item["key"], {})
366
+ req_data = res_data.get(req_item["key"], {})
366
367
  req._validate_response(req_data)
367
368
  if req_item["processor"]:
368
369
  data = req_item["processor"](req_data.get("data", req_data))
@@ -386,9 +387,8 @@ class RequestGroup(BaseRequest):
386
387
  @override
387
388
  def build_request_data(self):
388
389
  """构建请求"""
389
- common = self._build_common_params(self.credential)
390
390
  merged_data = {req["key"]: req["request"].data for req in self._requests}
391
- data = {"comm": common}
391
+ data = {"comm": self.common}
392
392
  data.update(merged_data)
393
393
  return data
394
394
 
@@ -403,7 +403,7 @@ class RequestGroup(BaseRequest):
403
403
  remove_index.append(idx)
404
404
  self._requests = [req for idx, req in enumerate(self._requests) if idx not in remove_index]
405
405
 
406
- async def execute(self) -> list[Any]:
406
+ async def _execute(self) -> list[Any]:
407
407
  """执行合并请求并返回各请求结果"""
408
408
  if not self._requests:
409
409
  return []
@@ -418,3 +418,33 @@ class RequestGroup(BaseRequest):
418
418
  resp = await self.request()
419
419
  await self._process_response(resp)
420
420
  return self._results
421
+
422
+ async def execute(self) -> list[Any]:
423
+ """执行合并请求"""
424
+ if not self._requests:
425
+ return []
426
+
427
+ # 未设置 limit 或请求数未超过 limit 时直接处理
428
+ if self.limit <= 0 or len(self._requests) <= self.limit:
429
+ return await self._execute()
430
+
431
+ # 分批次处理
432
+ batches = [self._requests[i : i + self.limit] for i in range(0, len(self._requests), self.limit)]
433
+ all_results = []
434
+
435
+ for batch in batches:
436
+ # 创建新 RequestGroup 处理当前批次
437
+ batch_group = RequestGroup(
438
+ common=self.common.copy(),
439
+ credential=self.credential,
440
+ )
441
+
442
+ # 添加当前批次的请求
443
+ for req_item in batch:
444
+ batch_group.add_request(req_item["request"], *req_item["args"], **req_item["kwargs"])
445
+
446
+ # 执行并收集结果
447
+ batch_results = await batch_group._execute()
448
+ all_results.extend(batch_results)
449
+
450
+ return all_results
@@ -1,7 +1,6 @@
1
1
  """QIMEI 获取"""
2
2
 
3
3
  import base64
4
- import json
5
4
  import logging
6
5
  import random
7
6
  from datetime import datetime, timedelta
@@ -9,6 +8,7 @@ from time import time
9
8
  from typing import TypedDict, cast
10
9
 
11
10
  import httpx
11
+ import orjson as json
12
12
  from cryptography.hazmat.primitives import serialization
13
13
  from cryptography.hazmat.primitives.asymmetric import padding
14
14
  from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey
@@ -112,7 +112,7 @@ def random_payload_by_device(device: Device, version: str) -> dict:
112
112
  "packageId": "com.tencent.qqmusic",
113
113
  "deviceType": "Phone",
114
114
  "sdkName": "",
115
- "reserved": json.dumps(reserved, separators=(",", ":"), ensure_ascii=False),
115
+ "reserved": json.dumps(reserved),
116
116
  }
117
117
 
118
118
 
@@ -125,7 +125,7 @@ def get_qimei(version: str) -> QimeiResult:
125
125
  nonce = "".join(random.choices("adbcdef1234567890", k=16))
126
126
  ts = int(time())
127
127
  key = base64.b64encode(rsa_encrypt(crypt_key.encode())).decode()
128
- params = base64.b64encode(aes_encrypt(crypt_key.encode(), json.dumps(payload).encode())).decode()
128
+ params = base64.b64encode(aes_encrypt(crypt_key.encode(), json.dumps(payload))).decode()
129
129
  extra = '{"appKey":"' + APP_KEY + '"}'
130
130
  sign = calc_md5(key, params, str(ts * 1000), nonce, SECRET, extra)
131
131
  res = httpx.post(
@@ -154,7 +154,7 @@ def get_qimei(version: str) -> QimeiResult:
154
154
  timeout=5,
155
155
  )
156
156
  logger.debug("获取 QIMEI 成功: %s", res.json())
157
- data = json.loads(res.json()["data"])["data"]
157
+ data = json.loads(json.loads(res.content)["data"])["data"]
158
158
  device.qimei = data["q36"]
159
159
  save_device(device)
160
160
  return QimeiResult(q16=data["q16"], q36=data["q36"])
@@ -4,8 +4,9 @@ import contextvars
4
4
  from typing import TypedDict
5
5
 
6
6
  import httpx
7
+ import orjson as json
7
8
  from aiocache import Cache
8
- from aiocache.serializers import JsonSerializer
9
+ from aiocache.serializers import BaseSerializer
9
10
 
10
11
  from .credential import Credential
11
12
  from .qimei import get_qimei
@@ -21,6 +22,20 @@ class ApiConfig(TypedDict):
21
22
  enc_endpoint: str
22
23
 
23
24
 
25
+ class ORJsonSerializer(BaseSerializer):
26
+ """Transform data to json string with json.dumps and json.loads to retrieve it back."""
27
+
28
+ def dumps(self, value):
29
+ """Serialize the received value using ``json.dumps``."""
30
+ return json.dumps(value, option=json.OPT_NON_STR_KEYS).decode()
31
+
32
+ def loads(self, value):
33
+ """Deserialize value using ``json.loads``."""
34
+ if value is None:
35
+ return None
36
+ return json.loads(value)
37
+
38
+
24
39
  class Session(httpx.AsyncClient):
25
40
  """Session 类,用于管理 QQ 音乐的登录态和 API 请求
26
41
 
@@ -41,9 +56,10 @@ class Session(httpx.AsyncClient):
41
56
  enable_sign: bool = False,
42
57
  enable_cache: bool = True,
43
58
  cache_ttl: int = 120,
59
+ http2: bool = True,
44
60
  **kwargs,
45
61
  ) -> None:
46
- super().__init__(**kwargs)
62
+ super().__init__(**kwargs, http2=http2)
47
63
  self.credential = credential
48
64
  self.headers.update(
49
65
  {
@@ -59,9 +75,15 @@ class Session(httpx.AsyncClient):
59
75
  enc_endpoint="https://u.y.qq.com/cgi-bin/musics.fcg",
60
76
  )
61
77
  self.enable_cache = enable_cache
62
- self._cache = Cache(serializer=JsonSerializer(), ttl=cache_ttl)
78
+ self._cache = Cache(serializer=ORJsonSerializer(), ttl=cache_ttl)
63
79
  self.qimei = get_qimei(self.api_config["version"])["q36"]
64
80
 
81
+ async def clear_cache(self):
82
+ """清除API请求缓存"""
83
+ if not self.enable_cache:
84
+ return
85
+ await self._cache.clear()
86
+
65
87
  async def __aenter__(self) -> "Session":
66
88
  """进入 async with 上下文时调用"""
67
89
  self._previous_session = _session_context.set(self)
qqmusic_api/utils/sign.py CHANGED
@@ -1,7 +1,8 @@
1
1
  """QQ音乐 sign"""
2
2
 
3
3
  import base64
4
- import json
4
+
5
+ import orjson as json
5
6
 
6
7
  from .common import calc_md5
7
8
 
@@ -56,7 +57,7 @@ def sign(request: dict) -> str:
56
57
  Returns:
57
58
  签名结果
58
59
  """
59
- md5_str = calc_md5(json.dumps(request, ensure_ascii=False, separators=(",", ":"))).upper().encode("utf-8")
60
+ md5_str = calc_md5(json.dumps(request)).upper().encode("utf-8")
60
61
 
61
62
  h = _head(md5_str)
62
63
  e = _tail(md5_str)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qqmusic-api-python
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: QQ音乐API封装库
5
5
  Project-URL: homepage, https://luren-dc.github.io/QQMusicApi/
6
6
  Project-URL: repository, https://github.com/luren-dc/QQMusicApi
@@ -23,7 +23,8 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
23
  Requires-Python: >=3.10
24
24
  Requires-Dist: aiocache>=0.12.3
25
25
  Requires-Dist: cryptography<44.0.2,>=44.0.1
26
- Requires-Dist: httpx>=0.27.0
26
+ Requires-Dist: httpx[http2]>=0.27.0
27
+ Requires-Dist: orjson>=3.10.15
27
28
  Requires-Dist: typing-extensions>=4.12.2
28
29
  Description-Content-Type: text/markdown
29
30
 
@@ -32,7 +33,7 @@ Description-Content-Type: text/markdown
32
33
  <img src="https://socialify.git.ci/luren-dc/QQMusicApi/image?description=1&font=Source%20Code%20Pro&language=1&logo=https%3A%2F%2Fy.qq.com%2Fmediastyle%2Fmod%2Fmobile%2Fimg%2Flogo.svg&name=1&pattern=Overlapping%20Hexagons&theme=Auto">
33
34
  </a>
34
35
  <a href="https://www.python.org">
35
- <img src="https://img.shields.io/badge/Python-3.10|3.11|3.12-blue" alt="Python">
36
+ <img src="https://img.shields.io/badge/Python-3.10|3.11|3.12|3.13-blue" alt="Python">
36
37
  </a>
37
38
  <a href="https://github.com/luren-dc/QQMusicApi?tab=MIT-1-ov-file">
38
39
  <img src="https://img.shields.io/github/license/luren-dc/QQMusicApi" alt="GitHub license">
@@ -0,0 +1,26 @@
1
+ qqmusic_api/__init__.py,sha256=82HP13mJMn69vktQVQCnorh4xdxWOHiOGnDAKFf3PT0,479
2
+ qqmusic_api/album.py,sha256=5kYuCizYzESVjlv5b6wdd30kgTBuuz4xFn6km3-31pI,1443
3
+ qqmusic_api/login.py,sha256=alz69NgJonxtMcCuEKTzNuOZov0lKeQgc-OUNXZDZ9Y,12643
4
+ qqmusic_api/lyric.py,sha256=NLB4CXfJg9ovsvc8XKy7b3Gw8lCASlDDXnqzRRTKs2w,1622
5
+ qqmusic_api/mv.py,sha256=FUGhfDDhfU9KMznZgbDaA8JnYwbYsQuwgVztBLaw6z8,1937
6
+ qqmusic_api/search.py,sha256=wRW9MylIkciOpcHoMPbJjaWOgw65-EtCJhKiNB3cKWg,3265
7
+ qqmusic_api/singer.py,sha256=mGtgYweeNYBZkv5nZmE5XSIcydzZWmf0ZbZz7cm50go,10186
8
+ qqmusic_api/song.py,sha256=TdYOB5blIc8BDhQXAxbObJM0u9DaHuefC-Yfian209o,9014
9
+ qqmusic_api/songlist.py,sha256=Bly5s8DO5azjCKZIrLllFhF-gmJCCxdInIZXih62gM8,1319
10
+ qqmusic_api/top.py,sha256=tZ17l0SbqJ0wDeRHJ7V5jvX4ZyoIEA-GCQHdZAIk648,792
11
+ qqmusic_api/user.py,sha256=IxCzQZAf7QpmeJ0uYJ62vskPDYbxiMPvyPcx1OhLPEk,6491
12
+ qqmusic_api/exceptions/__init__.py,sha256=KMohYPXJBcxigc1dpAioTPmPUHAQgOKXR4bkmv50EIQ,328
13
+ qqmusic_api/exceptions/api_exception.py,sha256=CTex5NMVNk42oJE0ADFFtAFWK4smchkgPTgRyRHb7O4,1609
14
+ qqmusic_api/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
+ qqmusic_api/utils/common.py,sha256=ntnuT5HlQvzeg5xX-netXS8tQZn2beE3bf3JJrmR7FU,2240
16
+ qqmusic_api/utils/credential.py,sha256=v_qbsyIvSNeAzXN-81Lpg3SdhjN1rmj6-qu9T0zk73Q,4280
17
+ qqmusic_api/utils/device.py,sha256=fTfRtEWvTWVtB533_fm6cpRvnWwQL3i8S6n-xyGXum8,2952
18
+ qqmusic_api/utils/network.py,sha256=TZnEcwTxf9K7WPrWWZqeF6M6x9e46t_K7VA5emP7nOc,14880
19
+ qqmusic_api/utils/qimei.py,sha256=qxKD6g-yybbDMZgLX2yFycAyXYMaHlOUvfbCHmqwAiQ,5758
20
+ qqmusic_api/utils/session.py,sha256=rLPgRMtkiFAx63Nym4TjaSlbMkEYVWB1T2xfyTRoIqQ,3446
21
+ qqmusic_api/utils/sign.py,sha256=WaG-93mJFv4FS7ueoxmMCOcnEW5KWRGYkePLceVc3_s,1363
22
+ qqmusic_api/utils/tripledes.py,sha256=v_ivJ64MD3AZ8soN_7wx_3wL0rSBXCDeqe0GyOHrXLk,15852
23
+ qqmusic_api_python-0.3.2.dist-info/METADATA,sha256=1P1Cp1tZ07kvQTuHmvPRKrAedVGSoml6s3yGUpcvtSw,4015
24
+ qqmusic_api_python-0.3.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
25
+ qqmusic_api_python-0.3.2.dist-info/licenses/LICENSE,sha256=dWHDhxdkwc4EVZ0xMf13_qTkjzPqbI1YL_1OzmZxaxU,1062
26
+ qqmusic_api_python-0.3.2.dist-info/RECORD,,
@@ -1,26 +0,0 @@
1
- qqmusic_api/__init__.py,sha256=0w9_ZhRtnX35WQ9PqphiKL38G3w4HzUtVfTfQN942Rg,479
2
- qqmusic_api/album.py,sha256=5kYuCizYzESVjlv5b6wdd30kgTBuuz4xFn6km3-31pI,1443
3
- qqmusic_api/login.py,sha256=ny4NSb-LAF6NvuaHZKZ-NJIZ7hvEnLwxbsxY6BWt0Gk,12651
4
- qqmusic_api/lyric.py,sha256=NLB4CXfJg9ovsvc8XKy7b3Gw8lCASlDDXnqzRRTKs2w,1622
5
- qqmusic_api/mv.py,sha256=FUGhfDDhfU9KMznZgbDaA8JnYwbYsQuwgVztBLaw6z8,1937
6
- qqmusic_api/search.py,sha256=wRW9MylIkciOpcHoMPbJjaWOgw65-EtCJhKiNB3cKWg,3265
7
- qqmusic_api/singer.py,sha256=jWjYnlIJEaZ4xQUZlF8WQ1_Xn8LGfb4rkNuhh99aLww,6999
8
- qqmusic_api/song.py,sha256=_glhO1xw41QEn_dBLmQJNePFwhbf5htGjYwVyXO3_pI,9043
9
- qqmusic_api/songlist.py,sha256=Bly5s8DO5azjCKZIrLllFhF-gmJCCxdInIZXih62gM8,1319
10
- qqmusic_api/top.py,sha256=tZ17l0SbqJ0wDeRHJ7V5jvX4ZyoIEA-GCQHdZAIk648,792
11
- qqmusic_api/user.py,sha256=IxCzQZAf7QpmeJ0uYJ62vskPDYbxiMPvyPcx1OhLPEk,6491
12
- qqmusic_api/exceptions/__init__.py,sha256=KMohYPXJBcxigc1dpAioTPmPUHAQgOKXR4bkmv50EIQ,328
13
- qqmusic_api/exceptions/api_exception.py,sha256=CTex5NMVNk42oJE0ADFFtAFWK4smchkgPTgRyRHb7O4,1609
14
- qqmusic_api/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
- qqmusic_api/utils/common.py,sha256=ntnuT5HlQvzeg5xX-netXS8tQZn2beE3bf3JJrmR7FU,2240
16
- qqmusic_api/utils/credential.py,sha256=B3z_x4oAkw8u0YGKnDOI-R9v2xHl0TSZdzRNgJL9FFY,3961
17
- qqmusic_api/utils/device.py,sha256=VwrfcGrT0kHItGP0UvxVMN0woD80kdyZKS0qKKCKWKU,2932
18
- qqmusic_api/utils/network.py,sha256=kiOUFkJcIv6J1gu-RKVQmI2vO7nL1ybQT8EqvpwwgiA,13864
19
- qqmusic_api/utils/qimei.py,sha256=ZMpgxKZBVAAdhp92liM1wCTF5TfYNmZhQ0gtaAVyKf0,5787
20
- qqmusic_api/utils/session.py,sha256=e8GKXJ5EtU1qHDrcD98xXaD_xcs4vu3XWHwOqQSe5LI,2758
21
- qqmusic_api/utils/sign.py,sha256=774CzI1WNHSh0CVkgY1MZLjgj7R5WVX8Q3ZUyU70lbk,1395
22
- qqmusic_api/utils/tripledes.py,sha256=v_ivJ64MD3AZ8soN_7wx_3wL0rSBXCDeqe0GyOHrXLk,15852
23
- qqmusic_api_python-0.3.0.dist-info/METADATA,sha256=Mx1LxA_MDXXzW-atmBt-S2gMC1f-ytlHnSQB7_1wxNI,3972
24
- qqmusic_api_python-0.3.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
25
- qqmusic_api_python-0.3.0.dist-info/licenses/LICENSE,sha256=dWHDhxdkwc4EVZ0xMf13_qTkjzPqbI1YL_1OzmZxaxU,1062
26
- qqmusic_api_python-0.3.0.dist-info/RECORD,,