qqmusic-api-python 0.2.1__tar.gz → 0.3.0__tar.gz
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_python-0.2.1 → qqmusic_api_python-0.3.0}/.gitignore +1 -0
- {qqmusic_api_python-0.2.1 → qqmusic_api_python-0.3.0}/PKG-INFO +9 -2
- {qqmusic_api_python-0.2.1 → qqmusic_api_python-0.3.0}/README.md +6 -0
- {qqmusic_api_python-0.2.1 → qqmusic_api_python-0.3.0}/pyproject.toml +6 -1
- qqmusic_api_python-0.3.0/qqmusic_api/__init__.py +27 -0
- qqmusic_api_python-0.3.0/qqmusic_api/album.py +58 -0
- qqmusic_api_python-0.3.0/qqmusic_api/login.py +440 -0
- qqmusic_api_python-0.3.0/qqmusic_api/lyric.py +68 -0
- qqmusic_api_python-0.3.0/qqmusic_api/mv.py +79 -0
- qqmusic_api_python-0.3.0/qqmusic_api/search.py +138 -0
- qqmusic_api_python-0.3.0/qqmusic_api/singer.py +277 -0
- qqmusic_api_python-0.3.0/qqmusic_api/song.py +321 -0
- qqmusic_api_python-0.3.0/qqmusic_api/songlist.py +49 -0
- qqmusic_api_python-0.3.0/qqmusic_api/top.py +34 -0
- qqmusic_api_python-0.3.0/qqmusic_api/user.py +206 -0
- {qqmusic_api_python-0.2.1 → qqmusic_api_python-0.3.0}/qqmusic_api/utils/common.py +0 -19
- {qqmusic_api_python-0.2.1 → qqmusic_api_python-0.3.0}/qqmusic_api/utils/credential.py +5 -31
- {qqmusic_api_python-0.2.1 → qqmusic_api_python-0.3.0}/qqmusic_api/utils/device.py +3 -2
- qqmusic_api_python-0.3.0/qqmusic_api/utils/network.py +420 -0
- {qqmusic_api_python-0.2.1 → qqmusic_api_python-0.3.0}/qqmusic_api/utils/qimei.py +13 -5
- qqmusic_api_python-0.3.0/qqmusic_api/utils/session.py +98 -0
- qqmusic_api_python-0.3.0/tests/test_album.py +13 -0
- qqmusic_api_python-0.3.0/tests/test_login.py +34 -0
- {qqmusic_api_python-0.2.1 → qqmusic_api_python-0.3.0}/tests/test_lyric.py +1 -2
- qqmusic_api_python-0.3.0/tests/test_mv.py +13 -0
- qqmusic_api_python-0.3.0/tests/test_network.py +51 -0
- qqmusic_api_python-0.3.0/tests/test_qimei.py +5 -0
- qqmusic_api_python-0.3.0/tests/test_session.py +94 -0
- qqmusic_api_python-0.3.0/tests/test_singer.py +34 -0
- qqmusic_api_python-0.3.0/tests/test_song.py +53 -0
- qqmusic_api_python-0.3.0/tests/test_songlist.py +14 -0
- qqmusic_api_python-0.3.0/tests/test_top.py +13 -0
- qqmusic_api_python-0.3.0/tests/test_user.py +70 -0
- qqmusic_api_python-0.2.1/qqmusic_api/__init__.py +0 -31
- qqmusic_api_python-0.2.1/qqmusic_api/album.py +0 -92
- qqmusic_api_python-0.2.1/qqmusic_api/data/api/album.json +0 -22
- qqmusic_api_python-0.2.1/qqmusic_api/data/api/login.json +0 -196
- qqmusic_api_python-0.2.1/qqmusic_api/data/api/lyric.json +0 -25
- qqmusic_api_python-0.2.1/qqmusic_api/data/api/mv.json +0 -26
- qqmusic_api_python-0.2.1/qqmusic_api/data/api/search.json +0 -74
- qqmusic_api_python-0.2.1/qqmusic_api/data/api/singer.json +0 -48
- qqmusic_api_python-0.2.1/qqmusic_api/data/api/song.json +0 -118
- qqmusic_api_python-0.2.1/qqmusic_api/data/api/songlist.json +0 -17
- qqmusic_api_python-0.2.1/qqmusic_api/data/api/top.json +0 -20
- qqmusic_api_python-0.2.1/qqmusic_api/data/api/user.json +0 -171
- qqmusic_api_python-0.2.1/qqmusic_api/data/file_type.json +0 -58
- qqmusic_api_python-0.2.1/qqmusic_api/data/search_type.json +0 -11
- qqmusic_api_python-0.2.1/qqmusic_api/login.py +0 -421
- qqmusic_api_python-0.2.1/qqmusic_api/login_utils.py +0 -141
- qqmusic_api_python-0.2.1/qqmusic_api/lyric.py +0 -68
- qqmusic_api_python-0.2.1/qqmusic_api/mv.py +0 -118
- qqmusic_api_python-0.2.1/qqmusic_api/search.py +0 -145
- qqmusic_api_python-0.2.1/qqmusic_api/singer.py +0 -223
- qqmusic_api_python-0.2.1/qqmusic_api/song.py +0 -389
- qqmusic_api_python-0.2.1/qqmusic_api/songlist.py +0 -58
- qqmusic_api_python-0.2.1/qqmusic_api/top.py +0 -52
- qqmusic_api_python-0.2.1/qqmusic_api/user.py +0 -277
- qqmusic_api_python-0.2.1/qqmusic_api/utils/network.py +0 -282
- qqmusic_api_python-0.2.1/qqmusic_api/utils/session.py +0 -162
- qqmusic_api_python-0.2.1/qqmusic_api/utils/sync.py +0 -31
- qqmusic_api_python-0.2.1/tests/conftest.py +0 -85
- qqmusic_api_python-0.2.1/tests/test_album.py +0 -11
- qqmusic_api_python-0.2.1/tests/test_login.py +0 -28
- qqmusic_api_python-0.2.1/tests/test_mv.py +0 -21
- qqmusic_api_python-0.2.1/tests/test_qimei.py +0 -6
- qqmusic_api_python-0.2.1/tests/test_singer.py +0 -26
- qqmusic_api_python-0.2.1/tests/test_song.py +0 -50
- qqmusic_api_python-0.2.1/tests/test_songlist.py +0 -15
- qqmusic_api_python-0.2.1/tests/test_top.py +0 -17
- qqmusic_api_python-0.2.1/tests/test_user.py +0 -65
- {qqmusic_api_python-0.2.1 → qqmusic_api_python-0.3.0}/LICENSE +0 -0
- {qqmusic_api_python-0.2.1 → qqmusic_api_python-0.3.0}/qqmusic_api/exceptions/__init__.py +0 -0
- {qqmusic_api_python-0.2.1 → qqmusic_api_python-0.3.0}/qqmusic_api/exceptions/api_exception.py +0 -0
- {qqmusic_api_python-0.2.1 → qqmusic_api_python-0.3.0}/qqmusic_api/utils/__init__.py +0 -0
- {qqmusic_api_python-0.2.1 → qqmusic_api_python-0.3.0}/qqmusic_api/utils/sign.py +0 -0
- {qqmusic_api_python-0.2.1 → qqmusic_api_python-0.3.0}/qqmusic_api/utils/tripledes.py +0 -0
- {qqmusic_api_python-0.2.1 → qqmusic_api_python-0.3.0}/tests/test_search.py +0 -0
- {qqmusic_api_python-0.2.1 → qqmusic_api_python-0.3.0}/tests/test_sign.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: qqmusic-api-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
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
|
|
@@ -21,7 +21,8 @@ Classifier: Programming Language :: Python :: 3.9
|
|
|
21
21
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
22
22
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
23
|
Requires-Python: >=3.10
|
|
24
|
-
Requires-Dist:
|
|
24
|
+
Requires-Dist: aiocache>=0.12.3
|
|
25
|
+
Requires-Dist: cryptography<44.0.2,>=44.0.1
|
|
25
26
|
Requires-Dist: httpx>=0.27.0
|
|
26
27
|
Requires-Dist: typing-extensions>=4.12.2
|
|
27
28
|
Description-Content-Type: text/markdown
|
|
@@ -94,6 +95,12 @@ async def main():
|
|
|
94
95
|
asyncio.run(main())
|
|
95
96
|
```
|
|
96
97
|
|
|
98
|
+
## Web API
|
|
99
|
+
```
|
|
100
|
+
docker build . -t qq-music-api
|
|
101
|
+
docker run -d -p 8000:8000 qq-music-api
|
|
102
|
+
```
|
|
103
|
+
|
|
97
104
|
## Licence
|
|
98
105
|
|
|
99
106
|
本项目基于 **[MIT License](https://github.com/luren-dc/QQMusicApi?tab=MIT-1-ov-file)** 许可证发行。
|
|
@@ -66,6 +66,12 @@ async def main():
|
|
|
66
66
|
asyncio.run(main())
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
+
## Web API
|
|
70
|
+
```
|
|
71
|
+
docker build . -t qq-music-api
|
|
72
|
+
docker run -d -p 8000:8000 qq-music-api
|
|
73
|
+
```
|
|
74
|
+
|
|
69
75
|
## Licence
|
|
70
76
|
|
|
71
77
|
本项目基于 **[MIT License](https://github.com/luren-dc/QQMusicApi?tab=MIT-1-ov-file)** 许可证发行。
|
|
@@ -5,9 +5,10 @@ authors = [
|
|
|
5
5
|
{ name = "Luren", email = "68656403+luren-dc@users.noreply.github.com" },
|
|
6
6
|
]
|
|
7
7
|
dependencies = [
|
|
8
|
-
"cryptography>=44.0.
|
|
8
|
+
"cryptography>=44.0.1,<44.0.2",
|
|
9
9
|
"typing-extensions>=4.12.2",
|
|
10
10
|
"httpx>=0.27.0",
|
|
11
|
+
"aiocache>=0.12.3",
|
|
11
12
|
]
|
|
12
13
|
requires-python = ">=3.10"
|
|
13
14
|
readme = "README.md"
|
|
@@ -67,6 +68,10 @@ docs = [
|
|
|
67
68
|
"docstring-inheritance>=2.2.1",
|
|
68
69
|
"griffe-modernized-annotations>=1.0.8",
|
|
69
70
|
]
|
|
71
|
+
web = [
|
|
72
|
+
"fastapi>=0.115.8",
|
|
73
|
+
"uvicorn>=0.34.0",
|
|
74
|
+
]
|
|
70
75
|
|
|
71
76
|
[tool.commitizen]
|
|
72
77
|
name = "cz_gitmoji"
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from . import album, login, lyric, mv, search, singer, song, songlist, top, user
|
|
4
|
+
from .utils.credential import Credential
|
|
5
|
+
from .utils.session import Session, get_session, set_session
|
|
6
|
+
|
|
7
|
+
__version__ = "0.3.0"
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger("qqmusicapi")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"Credential",
|
|
14
|
+
"Session",
|
|
15
|
+
"album",
|
|
16
|
+
"get_session",
|
|
17
|
+
"login",
|
|
18
|
+
"lyric",
|
|
19
|
+
"mv",
|
|
20
|
+
"search",
|
|
21
|
+
"set_session",
|
|
22
|
+
"singer",
|
|
23
|
+
"song",
|
|
24
|
+
"songlist",
|
|
25
|
+
"top",
|
|
26
|
+
"user",
|
|
27
|
+
]
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""专辑相关 API"""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Literal
|
|
4
|
+
|
|
5
|
+
from .utils.network import NO_PROCESSOR, api_request
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_cover(mid: str, size: Literal[150, 300, 500, 800] = 300) -> str:
|
|
9
|
+
"""获取专辑封面链接
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
mid: 专辑 mid
|
|
13
|
+
size: 封面大小
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
封面链接
|
|
17
|
+
"""
|
|
18
|
+
if size not in [150, 300, 500, 800]:
|
|
19
|
+
raise ValueError("not supported size")
|
|
20
|
+
return f"https://y.gtimg.cn/music/photo_new/T002R{size}x{size}M000{mid}.jpg"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@api_request("music.musichallAlbum.AlbumInfoServer", "GetAlbumDetail")
|
|
24
|
+
async def get_detail(value: str | int):
|
|
25
|
+
"""获取专辑详细信息
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
value: 专辑 id 或 mid
|
|
29
|
+
"""
|
|
30
|
+
if isinstance(value, int):
|
|
31
|
+
return {"albumId": value}, NO_PROCESSOR
|
|
32
|
+
|
|
33
|
+
return {"albumMId": value}, NO_PROCESSOR
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@api_request("music.musichallAlbum.AlbumSongList", "GetAlbumSongList")
|
|
37
|
+
async def get_song(value: str | int, num: int = 10, page: int = 1):
|
|
38
|
+
"""获取专辑歌曲
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
value: 专辑 id 或 mid
|
|
42
|
+
num: 返回数量
|
|
43
|
+
page: 页码
|
|
44
|
+
"""
|
|
45
|
+
params: dict[str, Any] = {
|
|
46
|
+
"begin": num * (page - 1),
|
|
47
|
+
"num": num,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
def _processor(data: dict[str, Any]) -> list[dict[str, Any]]:
|
|
51
|
+
return [song["songInfo"] for song in data["songList"]]
|
|
52
|
+
|
|
53
|
+
if isinstance(value, int):
|
|
54
|
+
params["albumId"] = value
|
|
55
|
+
else:
|
|
56
|
+
params["albumMid"] = value
|
|
57
|
+
|
|
58
|
+
return params, _processor
|
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
"""登录相关 API"""
|
|
2
|
+
|
|
3
|
+
import mimetypes
|
|
4
|
+
import random
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from time import time
|
|
10
|
+
from typing import Any
|
|
11
|
+
from uuid import uuid4
|
|
12
|
+
|
|
13
|
+
import httpx
|
|
14
|
+
|
|
15
|
+
from .exceptions.api_exception import CredentialExpiredError, LoginError
|
|
16
|
+
from .utils.common import hash33
|
|
17
|
+
from .utils.credential import Credential
|
|
18
|
+
from .utils.network import ApiRequest
|
|
19
|
+
from .utils.session import get_session
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
async def check_expired(credential: Credential) -> bool:
|
|
23
|
+
"""检查凭据是否过期
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
credential: 用户凭证
|
|
27
|
+
"""
|
|
28
|
+
api = ApiRequest(
|
|
29
|
+
"music.UserInfo.userInfoServer",
|
|
30
|
+
"GetLoginUserInfo",
|
|
31
|
+
params={},
|
|
32
|
+
credential=credential,
|
|
33
|
+
cacheable=False,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
await api()
|
|
38
|
+
return False
|
|
39
|
+
except CredentialExpiredError:
|
|
40
|
+
return True
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
async def refresh_cookies(credential: Credential) -> bool:
|
|
44
|
+
"""刷新 Cookies
|
|
45
|
+
|
|
46
|
+
Note:
|
|
47
|
+
刷新无效 cookie 需要 `refresh_key` 和 `refresh_token` 字段
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
credential: 用户凭证
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
是否刷新成功
|
|
54
|
+
"""
|
|
55
|
+
params = {
|
|
56
|
+
"refresh_key": credential.refresh_key,
|
|
57
|
+
"refresh_token": credential.refresh_token,
|
|
58
|
+
"musickey": credential.musickey,
|
|
59
|
+
"musicid": credential.musicid,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
api = ApiRequest[[], dict[str, Any]](
|
|
63
|
+
"music.login.LoginServer",
|
|
64
|
+
"Login",
|
|
65
|
+
common={"tmeLoginType": str(credential.login_type)},
|
|
66
|
+
params=params,
|
|
67
|
+
credential=credential,
|
|
68
|
+
cacheable=False,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
resp = await api()
|
|
73
|
+
c = credential.from_cookies_dict(resp)
|
|
74
|
+
credential.__dict__.update(c.__dict__)
|
|
75
|
+
return True
|
|
76
|
+
except CredentialExpiredError:
|
|
77
|
+
return False
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class QRCodeLoginEvents(Enum):
|
|
81
|
+
"""二维码登录状态
|
|
82
|
+
|
|
83
|
+
+ SCAN: 等待扫描二维码
|
|
84
|
+
+ CONF: 已扫码未确认登录
|
|
85
|
+
+ TIMEOUT: 二维码已过期
|
|
86
|
+
+ DONE: 扫码成功
|
|
87
|
+
+ REFUSE: 拒绝登录
|
|
88
|
+
+ OTHER: 未知情况
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
DONE = (0, 405)
|
|
92
|
+
SCAN = (66, 408)
|
|
93
|
+
CONF = (67, 404)
|
|
94
|
+
TIMEOUT = (65, None)
|
|
95
|
+
REFUSE = (68, 403)
|
|
96
|
+
OTHER = (None, None)
|
|
97
|
+
|
|
98
|
+
@classmethod
|
|
99
|
+
def get_by_value(cls, value: int):
|
|
100
|
+
"""根据传入的值查找对应的枚举成员"""
|
|
101
|
+
for member in cls:
|
|
102
|
+
if value in member.value:
|
|
103
|
+
return member
|
|
104
|
+
return cls.OTHER
|
|
105
|
+
|
|
106
|
+
""
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class PhoneLoginEvents(Enum):
|
|
110
|
+
"""手机登录状态
|
|
111
|
+
|
|
112
|
+
+ SEND: 发送成功
|
|
113
|
+
+ CAPTCHA: 需要滑块验证
|
|
114
|
+
+ FREQUENCY: 频繁操作
|
|
115
|
+
+ OTHER: 未知情况
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
SEND = 0
|
|
119
|
+
CAPTCHA = 20276
|
|
120
|
+
FREQUENCY = 100001
|
|
121
|
+
OTHER = None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class QRLoginType(Enum):
|
|
125
|
+
"""登录类型
|
|
126
|
+
|
|
127
|
+
+ QQ: QQ登录
|
|
128
|
+
+ WX: 微信登录
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
QQ = "qq"
|
|
132
|
+
WX = "wx"
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@dataclass()
|
|
136
|
+
class QR:
|
|
137
|
+
"""二维码
|
|
138
|
+
|
|
139
|
+
Attributes:
|
|
140
|
+
data: 二维码图像数据
|
|
141
|
+
qr_type: 二维码类型
|
|
142
|
+
mimetype: 二维码图像类型
|
|
143
|
+
identitfier: 标识符
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
data: bytes
|
|
147
|
+
qr_type: QRLoginType
|
|
148
|
+
mimetype: str
|
|
149
|
+
identifier: str
|
|
150
|
+
|
|
151
|
+
def save(self, path: Path | str = "."):
|
|
152
|
+
"""保存二维码
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
path: 保存文件夹
|
|
156
|
+
"""
|
|
157
|
+
if not self.data:
|
|
158
|
+
return None
|
|
159
|
+
path = Path(path)
|
|
160
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
161
|
+
file_path = (
|
|
162
|
+
path
|
|
163
|
+
/ f"{self.qr_type.value}-{uuid4()}{mimetypes.guess_extension(self.mimetype) if self.mimetype else None or '.png'}"
|
|
164
|
+
)
|
|
165
|
+
file_path.write_bytes(self.data)
|
|
166
|
+
return file_path
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
async def get_qrcode(login_type: QRLoginType) -> QR:
|
|
170
|
+
"""获取登录二维码"""
|
|
171
|
+
if login_type == QRLoginType.WX:
|
|
172
|
+
return await _get_wx_qr()
|
|
173
|
+
return await _get_qq_qr()
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
async def _get_qq_qr() -> QR:
|
|
177
|
+
res = await get_session().get(
|
|
178
|
+
"https://ssl.ptlogin2.qq.com/ptqrshow",
|
|
179
|
+
params={
|
|
180
|
+
"appid": "716027609",
|
|
181
|
+
"e": "2",
|
|
182
|
+
"l": "M",
|
|
183
|
+
"s": "3",
|
|
184
|
+
"d": "72",
|
|
185
|
+
"v": "4",
|
|
186
|
+
"t": str(random.random()),
|
|
187
|
+
"daid": "383",
|
|
188
|
+
"pt_3rd_aid": "100497308",
|
|
189
|
+
},
|
|
190
|
+
headers={"Referer": "https://xui.ptlogin2.qq.com/"},
|
|
191
|
+
)
|
|
192
|
+
qrsig = res.cookies.get("qrsig")
|
|
193
|
+
if not qrsig:
|
|
194
|
+
raise LoginError("[QQLogin] 获取二维码失败")
|
|
195
|
+
return QR(res.read(), QRLoginType.QQ, "image/png", qrsig)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
async def _get_wx_qr() -> QR:
|
|
199
|
+
session = get_session()
|
|
200
|
+
res = await session.get(
|
|
201
|
+
"https://open.weixin.qq.com/connect/qrconnect",
|
|
202
|
+
params={
|
|
203
|
+
"appid": "wx48db31d50e334801",
|
|
204
|
+
"redirect_uri": "https://y.qq.com/portal/wx_redirect.html?login_type=2&surl=https://y.qq.com/",
|
|
205
|
+
"response_type": "code",
|
|
206
|
+
"scope": "snsapi_login",
|
|
207
|
+
"state": "STATE",
|
|
208
|
+
"href": "https://y.qq.com/mediastyle/music_v17/src/css/popup_wechat.css#wechat_redirect",
|
|
209
|
+
},
|
|
210
|
+
)
|
|
211
|
+
uuid = re.findall(r"uuid=(.+?)\"", res.text)[0]
|
|
212
|
+
if not uuid:
|
|
213
|
+
raise LoginError("[WXLogin] 获取 uuid 失败")
|
|
214
|
+
qrcode_data = (
|
|
215
|
+
await session.get(
|
|
216
|
+
f"https://open.weixin.qq.com/connect/qrcode/{uuid}",
|
|
217
|
+
headers={"Referer": "https://open.weixin.qq.com/connect/qrconnect"},
|
|
218
|
+
)
|
|
219
|
+
).read()
|
|
220
|
+
return QR(qrcode_data, QRLoginType.WX, "image/jpeg", uuid)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
async def check_qrcode(qrcode: QR) -> tuple[QRCodeLoginEvents, Credential | None]:
|
|
224
|
+
"""检查二维码状态"""
|
|
225
|
+
if qrcode.qr_type == QRLoginType.WX:
|
|
226
|
+
return await _check_wx_qr(qrcode)
|
|
227
|
+
return await _check_qq_qr(qrcode)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
async def _check_qq_qr(qrcode: QR) -> tuple[QRCodeLoginEvents, Credential | None]:
|
|
231
|
+
qrsig = qrcode.identifier
|
|
232
|
+
try:
|
|
233
|
+
resp = await get_session().get(
|
|
234
|
+
"https://ssl.ptlogin2.qq.com/ptqrlogin",
|
|
235
|
+
params={
|
|
236
|
+
"u1": "https://graph.qq.com/oauth2.0/login_jump",
|
|
237
|
+
"ptqrtoken": hash33(qrsig),
|
|
238
|
+
"ptredirect": "0",
|
|
239
|
+
"h": "1",
|
|
240
|
+
"t": "1",
|
|
241
|
+
"g": "1",
|
|
242
|
+
"from_ui": "1",
|
|
243
|
+
"ptlang": "2052",
|
|
244
|
+
"action": f"0-0-{time() * 1000}",
|
|
245
|
+
"js_ver": "20102616",
|
|
246
|
+
"js_type": "1",
|
|
247
|
+
"pt_uistyle": "40",
|
|
248
|
+
"aid": "716027609",
|
|
249
|
+
"daid": "383",
|
|
250
|
+
"pt_3rd_aid": "100497308",
|
|
251
|
+
"has_onekey": "1",
|
|
252
|
+
},
|
|
253
|
+
headers={"Referer": "https://xui.ptlogin2.qq.com/", "Cookie": f"qrsig={qrsig}"},
|
|
254
|
+
)
|
|
255
|
+
except httpx.HTTPStatusError:
|
|
256
|
+
raise LoginError("[QQLogin] 无效 qrsig")
|
|
257
|
+
|
|
258
|
+
match = re.search(r"ptuiCB\((.*?)\)", resp.text)
|
|
259
|
+
if not match:
|
|
260
|
+
raise LoginError("[QQLogin] 获取二维码状态失败")
|
|
261
|
+
|
|
262
|
+
data = [p.strip("'") for p in match.group(1).split(",")]
|
|
263
|
+
code_str = data[0]
|
|
264
|
+
if code_str.isdigit():
|
|
265
|
+
event = QRCodeLoginEvents.get_by_value(int(code_str))
|
|
266
|
+
if event == QRCodeLoginEvents.DONE:
|
|
267
|
+
sigx = re.findall(r"&ptsigx=(.+?)&s_url", data[2])[0]
|
|
268
|
+
uin = re.findall(r"&uin=(.+?)&service", data[2])[0]
|
|
269
|
+
return event, await _authorize_qq_qr(uin, sigx)
|
|
270
|
+
return event, None
|
|
271
|
+
|
|
272
|
+
return QRCodeLoginEvents.OTHER, None
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
async def _check_wx_qr(qrcode: QR) -> tuple[QRCodeLoginEvents, Credential | None]:
|
|
276
|
+
uuid = qrcode.identifier
|
|
277
|
+
try:
|
|
278
|
+
resp = await get_session().get(
|
|
279
|
+
"https://lp.open.weixin.qq.com/connect/l/qrconnect",
|
|
280
|
+
params={"uuid": uuid, "_": str(int(time()) * 1000)},
|
|
281
|
+
headers={"Referer": "https://open.weixin.qq.com/"},
|
|
282
|
+
)
|
|
283
|
+
except httpx.TimeoutException:
|
|
284
|
+
return QRCodeLoginEvents.SCAN, None
|
|
285
|
+
|
|
286
|
+
match = re.search(r"window\.wx_errcode=(\d+);window\.wx_code=\'([^\']*)\'", resp.text)
|
|
287
|
+
if not match:
|
|
288
|
+
raise LoginError("[WXLogin] 获取二维码状态失败")
|
|
289
|
+
|
|
290
|
+
wx_errcode = match.group(1)
|
|
291
|
+
|
|
292
|
+
if not wx_errcode.isdigit():
|
|
293
|
+
return QRCodeLoginEvents.OTHER, None
|
|
294
|
+
|
|
295
|
+
event = QRCodeLoginEvents.get_by_value(int(wx_errcode))
|
|
296
|
+
|
|
297
|
+
if event == QRCodeLoginEvents.DONE:
|
|
298
|
+
wx_code = match.group(2)
|
|
299
|
+
if not wx_code:
|
|
300
|
+
raise LoginError("[WXLogin] 获取 code 失败")
|
|
301
|
+
|
|
302
|
+
return event, await _authorize_wx_qr(wx_code)
|
|
303
|
+
|
|
304
|
+
return event, None
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
async def _authorize_qq_qr(uin: str, sigx: str) -> Credential:
|
|
308
|
+
session = get_session()
|
|
309
|
+
resp = await session.get(
|
|
310
|
+
"https://ssl.ptlogin2.graph.qq.com/check_sig",
|
|
311
|
+
params={
|
|
312
|
+
"uin": uin,
|
|
313
|
+
"pttype": "1",
|
|
314
|
+
"service": "ptqrlogin",
|
|
315
|
+
"nodirect": "0",
|
|
316
|
+
"ptsigx": sigx,
|
|
317
|
+
"s_url": "https://graph.qq.com/oauth2.0/login_jump",
|
|
318
|
+
"ptlang": "2052",
|
|
319
|
+
"ptredirect": "100",
|
|
320
|
+
"aid": "716027609",
|
|
321
|
+
"daid": "383",
|
|
322
|
+
"j_later": "0",
|
|
323
|
+
"low_login_hour": "0",
|
|
324
|
+
"regmaster": "0",
|
|
325
|
+
"pt_login_type": "3",
|
|
326
|
+
"pt_aid": "0",
|
|
327
|
+
"pt_aaid": "16",
|
|
328
|
+
"pt_light": "0",
|
|
329
|
+
"pt_3rd_aid": "100497308",
|
|
330
|
+
},
|
|
331
|
+
headers={"Referer": "https://xui.ptlogin2.qq.com/"},
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
p_skey = resp.cookies.get("p_skey")
|
|
335
|
+
|
|
336
|
+
if not p_skey:
|
|
337
|
+
raise LoginError("[QQLogin] 获取 p_skey 失败")
|
|
338
|
+
|
|
339
|
+
resp = await session.post(
|
|
340
|
+
"https://graph.qq.com/oauth2.0/authorize",
|
|
341
|
+
data={
|
|
342
|
+
"response_type": "code",
|
|
343
|
+
"client_id": "100497308",
|
|
344
|
+
"redirect_uri": "https://y.qq.com/portal/wx_redirect.html?login_type=1&surl=https%3A%252F%252Fy.qq.com%252F",
|
|
345
|
+
"scope": "get_user_info,get_app_friends",
|
|
346
|
+
"state": "state",
|
|
347
|
+
"switch": "",
|
|
348
|
+
"from_ptlogin": "1",
|
|
349
|
+
"src": "1",
|
|
350
|
+
"update_auth": "1",
|
|
351
|
+
"openapi": "1010_1030",
|
|
352
|
+
"g_tk": hash33(p_skey, 5381),
|
|
353
|
+
"auth_time": str(int(time()) * 1000),
|
|
354
|
+
"ui": str(uuid4()),
|
|
355
|
+
},
|
|
356
|
+
)
|
|
357
|
+
location = resp.headers.get("Location", "")
|
|
358
|
+
try:
|
|
359
|
+
code = re.findall(r"(?<=code=)(.+?)(?=&)", location)[0]
|
|
360
|
+
except IndexError:
|
|
361
|
+
raise LoginError("[QQLogin] 获取 code 失败")
|
|
362
|
+
try:
|
|
363
|
+
api = ApiRequest[[], dict[str, Any]](
|
|
364
|
+
"music.login.LoginServer",
|
|
365
|
+
"Login",
|
|
366
|
+
common={"tmeLoginType": "2"},
|
|
367
|
+
params={"code": code},
|
|
368
|
+
cacheable=False,
|
|
369
|
+
)
|
|
370
|
+
return Credential.from_cookies_dict(await api())
|
|
371
|
+
except CredentialExpiredError:
|
|
372
|
+
raise LoginError("[QQLogin] 无法重复鉴权")
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
async def _authorize_wx_qr(code: str) -> Credential:
|
|
376
|
+
try:
|
|
377
|
+
api = ApiRequest[[], dict[str, Any]](
|
|
378
|
+
"music.login.LoginServer",
|
|
379
|
+
"Login",
|
|
380
|
+
common={"tmeLoginType": "1"},
|
|
381
|
+
params={"code": code, "strAppid": "wx48db31d50e334801"},
|
|
382
|
+
cacheable=False,
|
|
383
|
+
)
|
|
384
|
+
return Credential.from_cookies_dict(await api())
|
|
385
|
+
except CredentialExpiredError:
|
|
386
|
+
raise LoginError("[WXLogin] 无法重复鉴权")
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
async def send_authcode(phone: int, country_code: int = 86) -> tuple[PhoneLoginEvents, str | None]:
|
|
390
|
+
"""发送验证码
|
|
391
|
+
|
|
392
|
+
Args:
|
|
393
|
+
phone: 手机号
|
|
394
|
+
country_code: 国家码
|
|
395
|
+
"""
|
|
396
|
+
resp = await ApiRequest[[], dict[str, Any]](
|
|
397
|
+
"music.login.LoginServer",
|
|
398
|
+
"SendPhoneAuthCode",
|
|
399
|
+
common={"tmeLoginMethod": "3"},
|
|
400
|
+
params={
|
|
401
|
+
"tmeAppid": "qqmusic",
|
|
402
|
+
"phoneNo": str(phone),
|
|
403
|
+
"areaCode": str(country_code),
|
|
404
|
+
},
|
|
405
|
+
ignore_code=True,
|
|
406
|
+
cacheable=False,
|
|
407
|
+
)()
|
|
408
|
+
|
|
409
|
+
match resp["code"]:
|
|
410
|
+
case 20276:
|
|
411
|
+
return PhoneLoginEvents.CAPTCHA, resp["data"]["securityURL"]
|
|
412
|
+
case 100001:
|
|
413
|
+
return PhoneLoginEvents.FREQUENCY, None
|
|
414
|
+
case 0:
|
|
415
|
+
return PhoneLoginEvents.SEND, None
|
|
416
|
+
return PhoneLoginEvents.OTHER, resp["data"]["errMsg"]
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
async def phone_authorize(phone: int, auth_code: int, country_code: int = 86) -> Credential:
|
|
420
|
+
"""验证码鉴权
|
|
421
|
+
|
|
422
|
+
Args:
|
|
423
|
+
phone: 手机号
|
|
424
|
+
auth_code: 验证码
|
|
425
|
+
country_code: 国家码
|
|
426
|
+
"""
|
|
427
|
+
resp = await ApiRequest[[], dict[str, Any]](
|
|
428
|
+
"music.login.LoginServer",
|
|
429
|
+
"Login",
|
|
430
|
+
common={"tmeLoginMethod": "3", "tmeLoginType": "0"},
|
|
431
|
+
params={"code": str(auth_code), "phoneNo": str(phone), "areaCode": str(country_code), "loginMode": 1},
|
|
432
|
+
ignore_code=True,
|
|
433
|
+
cacheable=False,
|
|
434
|
+
)()
|
|
435
|
+
match resp["code"]:
|
|
436
|
+
case 20271:
|
|
437
|
+
raise LoginError("[PhoneLogin] 验证码错误或已鉴权")
|
|
438
|
+
case 0:
|
|
439
|
+
return Credential.from_cookies_dict(resp["data"])
|
|
440
|
+
raise LoginError("[PhoneLogin] 未知原因导致鉴权失败")
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""歌词 API"""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from qqmusic_api.utils.network import api_request
|
|
7
|
+
|
|
8
|
+
from .utils.common import qrc_decrypt
|
|
9
|
+
|
|
10
|
+
QRC_PATTERN = re.compile(r'<Lyric_.* LyricType=".*" LyricContent="(?P<content>.*?)"/>', re.DOTALL)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@api_request("music.musichallSong.PlayLyricInfo", "GetPlayLyricInfo")
|
|
14
|
+
async def get_lyric(
|
|
15
|
+
value: str | int,
|
|
16
|
+
qrc: bool = False,
|
|
17
|
+
trans: bool = False,
|
|
18
|
+
roma: bool = False,
|
|
19
|
+
):
|
|
20
|
+
"""获取歌词
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
value: 歌曲 id 或 mid
|
|
24
|
+
qrc: 是否返回逐字歌词
|
|
25
|
+
trans: 是否返回翻译歌词
|
|
26
|
+
roma: 是否返回罗马歌词
|
|
27
|
+
"""
|
|
28
|
+
params = {
|
|
29
|
+
"crypt": 1,
|
|
30
|
+
"ct": 11,
|
|
31
|
+
"cv": 13020508,
|
|
32
|
+
"lrc_t": 0,
|
|
33
|
+
"qrc": qrc,
|
|
34
|
+
"qrc_t": 0,
|
|
35
|
+
"roma": roma,
|
|
36
|
+
"roma_t": 0,
|
|
37
|
+
"trans": trans,
|
|
38
|
+
"trans_t": 0,
|
|
39
|
+
"type": 1,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if isinstance(value, int):
|
|
43
|
+
params["songId"] = value
|
|
44
|
+
else:
|
|
45
|
+
params["songMid"] = value
|
|
46
|
+
|
|
47
|
+
def _processor(data: dict[str, Any]):
|
|
48
|
+
lyric = qrc_decrypt(data["lyric"])
|
|
49
|
+
trans = qrc_decrypt(data["trans"])
|
|
50
|
+
roma = qrc_decrypt(data["roma"])
|
|
51
|
+
|
|
52
|
+
if lyric and qrc:
|
|
53
|
+
m_qrc = QRC_PATTERN.search(lyric)
|
|
54
|
+
if m_qrc and m_qrc.group("content"):
|
|
55
|
+
lyric = m_qrc.group("content")
|
|
56
|
+
|
|
57
|
+
if roma:
|
|
58
|
+
m_roma = QRC_PATTERN.search(roma)
|
|
59
|
+
if m_roma and m_roma.group("content"):
|
|
60
|
+
roma = m_roma.group("content")
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
"lyric": lyric,
|
|
64
|
+
"trans": trans,
|
|
65
|
+
"roma": roma,
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return params, _processor
|