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 +1 -1
- qqmusic_api/login.py +0 -2
- qqmusic_api/singer.py +192 -46
- qqmusic_api/song.py +0 -1
- qqmusic_api/utils/credential.py +11 -3
- qqmusic_api/utils/device.py +3 -2
- qqmusic_api/utils/network.py +41 -11
- qqmusic_api/utils/qimei.py +4 -4
- qqmusic_api/utils/session.py +25 -3
- qqmusic_api/utils/sign.py +3 -2
- {qqmusic_api_python-0.3.0.dist-info → qqmusic_api_python-0.3.2.dist-info}/METADATA +4 -3
- qqmusic_api_python-0.3.2.dist-info/RECORD +26 -0
- qqmusic_api_python-0.3.0.dist-info/RECORD +0 -26
- {qqmusic_api_python-0.3.0.dist-info → qqmusic_api_python-0.3.2.dist-info}/WHEEL +0 -0
- {qqmusic_api_python-0.3.0.dist-info → qqmusic_api_python-0.3.2.dist-info}/licenses/LICENSE +0 -0
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.
|
|
7
|
+
__version__ = "0.3.2"
|
|
8
8
|
|
|
9
9
|
logger = logging.getLogger("qqmusicapi")
|
|
10
10
|
|
qqmusic_api/login.py
CHANGED
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
78
|
-
return value #
|
|
79
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
qqmusic_api/utils/credential.py
CHANGED
|
@@ -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
|
|
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(
|
qqmusic_api/utils/device.py
CHANGED
|
@@ -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())
|
qqmusic_api/utils/network.py
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
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,
|
|
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.
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
qqmusic_api/utils/qimei.py
CHANGED
|
@@ -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
|
|
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)
|
|
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.
|
|
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"])
|
qqmusic_api/utils/session.py
CHANGED
|
@@ -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
|
|
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=
|
|
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
|
-
|
|
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
|
|
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.
|
|
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,,
|
|
File without changes
|
|
File without changes
|