qqmusic-api-python 0.1.0__tar.gz → 0.1.1__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.1.0 → qqmusic_api_python-0.1.1}/PKG-INFO +14 -10
- {qqmusic_api_python-0.1.0 → qqmusic_api_python-0.1.1}/README.md +10 -6
- {qqmusic_api_python-0.1.0 → qqmusic_api_python-0.1.1}/pyproject.toml +15 -16
- qqmusic_api_python-0.1.1/qqmusic_api/__init__.py +14 -0
- {qqmusic_api_python-0.1.0 → qqmusic_api_python-0.1.1}/qqmusic_api/api/album.py +3 -0
- {qqmusic_api_python-0.1.0 → qqmusic_api_python-0.1.1}/qqmusic_api/api/login.py +9 -14
- {qqmusic_api_python-0.1.0 → qqmusic_api_python-0.1.1}/qqmusic_api/api/mv.py +3 -0
- qqmusic_api_python-0.1.1/qqmusic_api/api/singer.py +260 -0
- {qqmusic_api_python-0.1.0 → qqmusic_api_python-0.1.1}/qqmusic_api/api/song.py +75 -21
- {qqmusic_api_python-0.1.0 → qqmusic_api_python-0.1.1}/qqmusic_api/api/songlist.py +11 -3
- {qqmusic_api_python-0.1.0 → qqmusic_api_python-0.1.1}/qqmusic_api/api/top.py +13 -8
- qqmusic_api_python-0.1.1/qqmusic_api/data/api/singer.json +48 -0
- qqmusic_api_python-0.1.1/qqmusic_api/exceptions.py +106 -0
- {qqmusic_api_python-0.1.0 → qqmusic_api_python-0.1.1}/qqmusic_api/utils/credential.py +27 -26
- {qqmusic_api_python-0.1.0 → qqmusic_api_python-0.1.1}/qqmusic_api/utils/network.py +11 -12
- qqmusic_api_python-0.1.0/qqmusic_api/__init__.py +0 -4
- qqmusic_api_python-0.1.0/qqmusic_api/data/api/singer.json +0 -1
- qqmusic_api_python-0.1.0/qqmusic_api/exceptions.py +0 -121
- {qqmusic_api_python-0.1.0 → qqmusic_api_python-0.1.1}/LICENSE +0 -0
- {qqmusic_api_python-0.1.0 → qqmusic_api_python-0.1.1}/qqmusic_api/api/search.py +0 -0
- {qqmusic_api_python-0.1.0 → qqmusic_api_python-0.1.1}/qqmusic_api/data/api/album.json +0 -0
- {qqmusic_api_python-0.1.0 → qqmusic_api_python-0.1.1}/qqmusic_api/data/api/login.json +0 -0
- {qqmusic_api_python-0.1.0 → qqmusic_api_python-0.1.1}/qqmusic_api/data/api/mv.json +0 -0
- {qqmusic_api_python-0.1.0 → qqmusic_api_python-0.1.1}/qqmusic_api/data/api/search.json +0 -0
- {qqmusic_api_python-0.1.0 → qqmusic_api_python-0.1.1}/qqmusic_api/data/api/song.json +0 -0
- {qqmusic_api_python-0.1.0 → qqmusic_api_python-0.1.1}/qqmusic_api/data/api/songlist.json +0 -0
- {qqmusic_api_python-0.1.0 → qqmusic_api_python-0.1.1}/qqmusic_api/data/api/top.json +0 -0
- {qqmusic_api_python-0.1.0 → qqmusic_api_python-0.1.1}/qqmusic_api/data/file_type.json +0 -0
- {qqmusic_api_python-0.1.0 → qqmusic_api_python-0.1.1}/qqmusic_api/data/search_type.json +0 -0
- {qqmusic_api_python-0.1.0 → qqmusic_api_python-0.1.1}/qqmusic_api/settings.py +0 -0
- {qqmusic_api_python-0.1.0 → qqmusic_api_python-0.1.1}/qqmusic_api/utils/__init__.py +0 -0
- {qqmusic_api_python-0.1.0 → qqmusic_api_python-0.1.1}/qqmusic_api/utils/common.py +0 -0
- {qqmusic_api_python-0.1.0 → qqmusic_api_python-0.1.1}/qqmusic_api/utils/qimei.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: qqmusic-api-python
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
4
4
|
Summary: QQ音乐API封装库
|
|
5
5
|
Home-page: https://github.com/luren-dc/QQMusicApi
|
|
6
6
|
License: MIT
|
|
@@ -24,15 +24,15 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
24
24
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
25
25
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
26
26
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
27
|
-
Requires-Dist: aiohttp (
|
|
28
|
-
Requires-Dist: cryptography (
|
|
29
|
-
Requires-Dist: requests (
|
|
27
|
+
Requires-Dist: aiohttp (>=3.9.5,<4.0.0)
|
|
28
|
+
Requires-Dist: cryptography (>=41.0.2,<42.0.0)
|
|
29
|
+
Requires-Dist: requests (>=2.31.0,<3.0.0)
|
|
30
30
|
Project-URL: Documentation, https://github.com/luren-dc/QQMusicApi
|
|
31
31
|
Project-URL: Repository, https://github.com/luren-dc/QQMusicApi
|
|
32
32
|
Description-Content-Type: text/markdown
|
|
33
33
|
|
|
34
34
|
<div align="center">
|
|
35
|
-
<h1>
|
|
35
|
+
<h1> QQMusic Api </h1>
|
|
36
36
|
<p> Python QQ音乐 API 封装库 </p>
|
|
37
37
|
|
|
38
38
|

|
|
@@ -46,6 +46,8 @@ Description-Content-Type: text/markdown
|
|
|
46
46
|
> [!WARNING]
|
|
47
47
|
> 本仓库的所有内容仅供学习和参考之用,禁止用于商业用途。
|
|
48
48
|
|
|
49
|
+
文档: [https://luren-dc.github.io/QQMusicApi/](https://luren-dc.github.io/QQMusicApi/)
|
|
50
|
+
|
|
49
51
|
## 介绍
|
|
50
52
|
|
|
51
53
|
使用 Python 编写的用于调用 [QQ音乐](https://y.qq.com/) 各种 API 的库.
|
|
@@ -62,13 +64,13 @@ Description-Content-Type: text/markdown
|
|
|
62
64
|
|
|
63
65
|
### 安装
|
|
64
66
|
|
|
65
|
-
```
|
|
66
|
-
$
|
|
67
|
+
```shell
|
|
68
|
+
$ pip install qqmusic-api-python
|
|
67
69
|
```
|
|
68
70
|
|
|
69
71
|
### 使用
|
|
70
72
|
|
|
71
|
-
```
|
|
73
|
+
```python
|
|
72
74
|
import asyncio
|
|
73
75
|
|
|
74
76
|
from qqmusic_api import search
|
|
@@ -86,14 +88,16 @@ if __name__ == "__main__":
|
|
|
86
88
|
## TODO
|
|
87
89
|
|
|
88
90
|
- [ ] 歌手 API
|
|
91
|
+
- [ ] 评论 API
|
|
92
|
+
- [ ] 用户 API
|
|
89
93
|
|
|
90
94
|
## 参考项目
|
|
91
95
|
|
|
92
|
-
- [Rain120/qq-muisc-api](https://github.com/Rain120/qq-
|
|
96
|
+
- [Rain120/qq-muisc-api](https://github.com/Rain120/qq-music-api)
|
|
93
97
|
- [jsososo/QQMusicApi](https://github.com/jsososo/QQMusicApi)
|
|
94
98
|
- [Nemo2011/bilibili-api](https://github.com/Nemo2011/bilibili-api/)
|
|
95
99
|
|
|
96
100
|
## Licence
|
|
97
101
|
|
|
98
|
-
**[MIT License](https://github.com/luren-dc/QQMusicApi
|
|
102
|
+
**[MIT License](https://github.com/luren-dc/QQMusicApi?tab=MIT-1-ov-file)**
|
|
99
103
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<div align="center">
|
|
2
|
-
<h1>
|
|
2
|
+
<h1> QQMusic Api </h1>
|
|
3
3
|
<p> Python QQ音乐 API 封装库 </p>
|
|
4
4
|
|
|
5
5
|

|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
> [!WARNING]
|
|
14
14
|
> 本仓库的所有内容仅供学习和参考之用,禁止用于商业用途。
|
|
15
15
|
|
|
16
|
+
文档: [https://luren-dc.github.io/QQMusicApi/](https://luren-dc.github.io/QQMusicApi/)
|
|
17
|
+
|
|
16
18
|
## 介绍
|
|
17
19
|
|
|
18
20
|
使用 Python 编写的用于调用 [QQ音乐](https://y.qq.com/) 各种 API 的库.
|
|
@@ -29,13 +31,13 @@
|
|
|
29
31
|
|
|
30
32
|
### 安装
|
|
31
33
|
|
|
32
|
-
```
|
|
33
|
-
$
|
|
34
|
+
```shell
|
|
35
|
+
$ pip install qqmusic-api-python
|
|
34
36
|
```
|
|
35
37
|
|
|
36
38
|
### 使用
|
|
37
39
|
|
|
38
|
-
```
|
|
40
|
+
```python
|
|
39
41
|
import asyncio
|
|
40
42
|
|
|
41
43
|
from qqmusic_api import search
|
|
@@ -53,13 +55,15 @@ if __name__ == "__main__":
|
|
|
53
55
|
## TODO
|
|
54
56
|
|
|
55
57
|
- [ ] 歌手 API
|
|
58
|
+
- [ ] 评论 API
|
|
59
|
+
- [ ] 用户 API
|
|
56
60
|
|
|
57
61
|
## 参考项目
|
|
58
62
|
|
|
59
|
-
- [Rain120/qq-muisc-api](https://github.com/Rain120/qq-
|
|
63
|
+
- [Rain120/qq-muisc-api](https://github.com/Rain120/qq-music-api)
|
|
60
64
|
- [jsososo/QQMusicApi](https://github.com/jsososo/QQMusicApi)
|
|
61
65
|
- [Nemo2011/bilibili-api](https://github.com/Nemo2011/bilibili-api/)
|
|
62
66
|
|
|
63
67
|
## Licence
|
|
64
68
|
|
|
65
|
-
**[MIT License](https://github.com/luren-dc/QQMusicApi
|
|
69
|
+
**[MIT License](https://github.com/luren-dc/QQMusicApi?tab=MIT-1-ov-file)**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "qqmusic-api-python"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.1"
|
|
4
4
|
description = "QQ音乐API封装库"
|
|
5
5
|
authors = ["Luren <dluren.c@gmail.com>"]
|
|
6
6
|
packages = [{ include = "qqmusic_api" }]
|
|
@@ -26,18 +26,22 @@ classifiers = [
|
|
|
26
26
|
|
|
27
27
|
[tool.poetry.dependencies]
|
|
28
28
|
python = "^3.9"
|
|
29
|
-
cryptography = "41.0.2"
|
|
30
|
-
requests = "2.31.0"
|
|
31
|
-
aiohttp = "3.9.5"
|
|
29
|
+
cryptography = "^41.0.2"
|
|
30
|
+
requests = "^2.31.0"
|
|
31
|
+
aiohttp = "^3.9.5"
|
|
32
32
|
|
|
33
33
|
[tool.poetry.group.dev.dependencies]
|
|
34
|
-
pytest = "8.2.0"
|
|
35
|
-
pytest-asyncio = "0.23.6"
|
|
36
|
-
qrcode = { extras = ["pil"], version = "7.4.2" }
|
|
37
|
-
pyzbar = "0.1.9"
|
|
38
|
-
pytest-timeout = "2.3.1"
|
|
39
|
-
pytest-sugar = "1.0.0"
|
|
40
|
-
pre-commit = "3.7.0"
|
|
34
|
+
pytest = "^8.2.0"
|
|
35
|
+
pytest-asyncio = "^0.23.6"
|
|
36
|
+
qrcode = { extras = ["pil"], version = "^7.4.2" }
|
|
37
|
+
pyzbar = "^0.1.9"
|
|
38
|
+
pytest-timeout = "^2.3.1"
|
|
39
|
+
pytest-sugar = "^1.0.0"
|
|
40
|
+
pre-commit = "^3.7.0"
|
|
41
|
+
|
|
42
|
+
[build-system]
|
|
43
|
+
requires = ["poetry-core"]
|
|
44
|
+
build-backend = "poetry.core.masonry.api"
|
|
41
45
|
|
|
42
46
|
[tool.mypy]
|
|
43
47
|
disable_error_code = ["index", "arg-type", "union-attr", "return-value"]
|
|
@@ -48,8 +52,3 @@ show_column_numbers = true
|
|
|
48
52
|
pythonpath = "./"
|
|
49
53
|
timeout = 30
|
|
50
54
|
testpaths = ["tests"]
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
[build-system]
|
|
54
|
-
requires = ["poetry-core"]
|
|
55
|
-
build-backend = "poetry.core.masonry.api"
|
|
@@ -5,7 +5,6 @@ from abc import ABC, abstractmethod
|
|
|
5
5
|
from enum import Enum
|
|
6
6
|
from typing import Optional
|
|
7
7
|
|
|
8
|
-
from ..exceptions import AuthcodeExpiredException, ResponseCodeException
|
|
9
8
|
from ..utils.common import get_api, hash33, random_uuid
|
|
10
9
|
from ..utils.credential import Credential
|
|
11
10
|
from ..utils.network import Api, get_aiohttp_session
|
|
@@ -15,7 +14,7 @@ API = get_api("login")
|
|
|
15
14
|
|
|
16
15
|
class QrCodeLoginEvents(Enum):
|
|
17
16
|
"""
|
|
18
|
-
|
|
17
|
+
二维码登录状态
|
|
19
18
|
|
|
20
19
|
+ SCAN: 未扫描二维码
|
|
21
20
|
+ CONF: 未确认登录
|
|
@@ -35,7 +34,7 @@ class QrCodeLoginEvents(Enum):
|
|
|
35
34
|
|
|
36
35
|
class PhoneLoginEvents(Enum):
|
|
37
36
|
"""
|
|
38
|
-
|
|
37
|
+
手机登录状态
|
|
39
38
|
|
|
40
39
|
+ SEND: 发送成功
|
|
41
40
|
+ CAPTCHA: 需要滑块验证
|
|
@@ -400,17 +399,13 @@ class PhoneLogin(Login):
|
|
|
400
399
|
if not authcode:
|
|
401
400
|
raise ValueError("authcode 为空")
|
|
402
401
|
params = {"code": str(authcode), "phoneNo": str(self.phone), "loginMode": 1}
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
return Credential.from_cookies(res)
|
|
411
|
-
except ResponseCodeException as e:
|
|
412
|
-
if e.code == 20271:
|
|
413
|
-
raise AuthcodeExpiredException()
|
|
402
|
+
res = (
|
|
403
|
+
await Api(**API["phone_login"])
|
|
404
|
+
.update_params(**params)
|
|
405
|
+
.update_extra_common(tmeLoginMethod="3")
|
|
406
|
+
.result
|
|
407
|
+
)
|
|
408
|
+
return Credential.from_cookies(res)
|
|
414
409
|
|
|
415
410
|
|
|
416
411
|
async def refresh_cookies(credential: Credential) -> Credential:
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from ..utils.common import get_api
|
|
5
|
+
from ..utils.network import Api
|
|
6
|
+
from .song import Song
|
|
7
|
+
|
|
8
|
+
API = get_api("singer")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AreaType(Enum):
|
|
12
|
+
"""
|
|
13
|
+
地区
|
|
14
|
+
|
|
15
|
+
+ ALL: 全部
|
|
16
|
+
+ CHINA: 内地
|
|
17
|
+
+ TAIWAN: 台湾
|
|
18
|
+
+ AMERICA: 美国
|
|
19
|
+
+ EUROPE: 欧美
|
|
20
|
+
+ JAPAN: 日本
|
|
21
|
+
+ KOREA: 韩国
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
ALL = -100
|
|
25
|
+
CHINA = 200
|
|
26
|
+
TAIWAN = 2
|
|
27
|
+
AMERICA = 5
|
|
28
|
+
EUROPE = 4
|
|
29
|
+
JAPAN = 3
|
|
30
|
+
KOREA = 1
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class GenreType(Enum):
|
|
34
|
+
"""
|
|
35
|
+
风格
|
|
36
|
+
|
|
37
|
+
+ ALL: 全部
|
|
38
|
+
+ POP: 流行
|
|
39
|
+
+ RAP: 说唱
|
|
40
|
+
+ CHINESE_STYLE: 国风
|
|
41
|
+
+ ROCK: 摇滚
|
|
42
|
+
+ ELECTRONIC: 电子
|
|
43
|
+
+ FOLK: 民谣
|
|
44
|
+
+ R_AND_B: R&B
|
|
45
|
+
+ ETHNIC: 民族乐
|
|
46
|
+
+ LIGHT_MUSIC: 轻音乐
|
|
47
|
+
+ JAZZ: 爵士
|
|
48
|
+
+ CLASSICAL: 古典
|
|
49
|
+
+ COUNTRY: 乡村
|
|
50
|
+
+ BLUES: 蓝调
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
ALL = -100
|
|
54
|
+
POP = 7
|
|
55
|
+
RAP = 3
|
|
56
|
+
CHINESE_STYLE = 19
|
|
57
|
+
ROCK = 4
|
|
58
|
+
ELECTRONIC = 2
|
|
59
|
+
FOLK = 8
|
|
60
|
+
R_AND_B = 11
|
|
61
|
+
ETHNIC = 37
|
|
62
|
+
LIGHT_MUSIC = 93
|
|
63
|
+
JAZZ = 14
|
|
64
|
+
CLASSICAL = 33
|
|
65
|
+
COUNTRY = 13
|
|
66
|
+
BLUES = 10
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class SexType(Enum):
|
|
70
|
+
"""
|
|
71
|
+
性别
|
|
72
|
+
|
|
73
|
+
+ ALL: 全部
|
|
74
|
+
+ MALE: 男
|
|
75
|
+
+ FEMALE: 女
|
|
76
|
+
+ GROUP: 组合
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
ALL = -100
|
|
80
|
+
MALE = 0
|
|
81
|
+
FEMALE = 1
|
|
82
|
+
GROUP = 2
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class TabType(Enum):
|
|
86
|
+
"""
|
|
87
|
+
Tab 类型
|
|
88
|
+
|
|
89
|
+
+ WIKI: wiki
|
|
90
|
+
+ ALBUM: 专辑
|
|
91
|
+
+ COMPOSER: 作曲
|
|
92
|
+
+ LYRICIST: 作词
|
|
93
|
+
+ PRODUCER: 制作人
|
|
94
|
+
+ ARRANGER: 编曲
|
|
95
|
+
+ MUSICIAN: 乐手
|
|
96
|
+
+ SONG: 歌曲
|
|
97
|
+
+ VIDEO: 视频
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
WIKI = ("wiki", "IntroductionTab")
|
|
101
|
+
ALBUM = ("album", "AlbumTab")
|
|
102
|
+
COMPOSER = ("song_composing", "SongTab")
|
|
103
|
+
LYRICIST = ("song_lyric", "SongTab")
|
|
104
|
+
PRODUCER = ("producer", "SongTab")
|
|
105
|
+
ARRANGER = ("arranger", "SongTab")
|
|
106
|
+
MUSICIAN = ("musician", "SongTab")
|
|
107
|
+
SONG = ("song_sing", "SongTab")
|
|
108
|
+
VIDEO = ("video", "VideoTab")
|
|
109
|
+
|
|
110
|
+
def __init__(self, tabID: str, tabName: str) -> None:
|
|
111
|
+
super().__init__()
|
|
112
|
+
self.tabID = tabID
|
|
113
|
+
self.tabName = tabName
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
async def get_singer_list(
|
|
117
|
+
area: AreaType = AreaType.ALL,
|
|
118
|
+
sex: SexType = SexType.ALL,
|
|
119
|
+
genre: GenreType = GenreType.ALL,
|
|
120
|
+
) -> list:
|
|
121
|
+
"""
|
|
122
|
+
获取歌手列表
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
area: 地区.Defaluts to AreaType.ALL
|
|
126
|
+
sex: 性别.Defaluts to SexType.ALL
|
|
127
|
+
genre: 风格.Defaluts to GenreType.ALL
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
list: 歌手列表
|
|
131
|
+
"""
|
|
132
|
+
result = (
|
|
133
|
+
await Api(**API["singer_list"])
|
|
134
|
+
.update_params(
|
|
135
|
+
hastag=0,
|
|
136
|
+
area=area.value,
|
|
137
|
+
sex=sex.value,
|
|
138
|
+
genre=genre.value,
|
|
139
|
+
)
|
|
140
|
+
.result
|
|
141
|
+
)
|
|
142
|
+
return result["hotlist"]
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class Singer:
|
|
146
|
+
"""
|
|
147
|
+
歌手类
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
def __init__(self, mid: str) -> None:
|
|
151
|
+
"""
|
|
152
|
+
Args:
|
|
153
|
+
mid: 歌手 mid
|
|
154
|
+
"""
|
|
155
|
+
self.mid = mid
|
|
156
|
+
self._info: Optional[dict] = None
|
|
157
|
+
|
|
158
|
+
def __repr__(self) -> str:
|
|
159
|
+
return f"Singer(mid={self.mid})"
|
|
160
|
+
|
|
161
|
+
def __str__(self) -> str:
|
|
162
|
+
if self._info:
|
|
163
|
+
return str(self._info)
|
|
164
|
+
return f"Singer(mid={self.mid})"
|
|
165
|
+
|
|
166
|
+
async def __get_info(self) -> dict:
|
|
167
|
+
"""
|
|
168
|
+
获取歌手必要信息
|
|
169
|
+
"""
|
|
170
|
+
if not self._info:
|
|
171
|
+
info = (
|
|
172
|
+
await Api(**API["homepage"]).update_params(SingerMid=self.mid).result
|
|
173
|
+
)["Info"]
|
|
174
|
+
self._info = {
|
|
175
|
+
"FansNum": info["FansNum"]["Num"],
|
|
176
|
+
}
|
|
177
|
+
self._info.update(info["Singer"])
|
|
178
|
+
return self._info
|
|
179
|
+
|
|
180
|
+
async def get_info(self) -> dict:
|
|
181
|
+
"""
|
|
182
|
+
获取歌手信息
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
dict: 歌手信息
|
|
186
|
+
"""
|
|
187
|
+
if not self._info:
|
|
188
|
+
self._info = await self.__get_info()
|
|
189
|
+
return self._info
|
|
190
|
+
|
|
191
|
+
async def get_fans_num(self) -> int:
|
|
192
|
+
"""
|
|
193
|
+
获取歌手粉丝数
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
int: 粉丝数
|
|
197
|
+
"""
|
|
198
|
+
return (await self.__get_info())["FansNum"]
|
|
199
|
+
|
|
200
|
+
async def get_tab_detail(self, tab_type: TabType, page: int = 1, num: int = 100):
|
|
201
|
+
"""
|
|
202
|
+
获取歌手 Tab 详细信息
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
tab_type: Tab 类型
|
|
206
|
+
page: 页码
|
|
207
|
+
num: 返回数量
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
list: Tab 详细信息
|
|
211
|
+
"""
|
|
212
|
+
return (
|
|
213
|
+
await Api(**API["homepage_tab_detail"])
|
|
214
|
+
.update_params(
|
|
215
|
+
SingerMid=self.mid,
|
|
216
|
+
IsQueryTabDetail=1,
|
|
217
|
+
TabID=tab_type.tabID,
|
|
218
|
+
PageNum=page - 1,
|
|
219
|
+
PageSize=num,
|
|
220
|
+
Order=0,
|
|
221
|
+
)
|
|
222
|
+
.result
|
|
223
|
+
)[tab_type.tabName]
|
|
224
|
+
|
|
225
|
+
async def get_wiki(self) -> dict:
|
|
226
|
+
"""
|
|
227
|
+
获取歌手WiKi
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
dict: 歌手WiKi
|
|
231
|
+
"""
|
|
232
|
+
return await self.get_tab_detail(TabType.WIKI)
|
|
233
|
+
|
|
234
|
+
async def get_song(
|
|
235
|
+
self, t: TabType = TabType.SONG, page: int = 1, num: int = 100
|
|
236
|
+
) -> list[Song]:
|
|
237
|
+
"""
|
|
238
|
+
获取歌手歌曲
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
t: Tab 类型. Defaluts to TabType.SONG
|
|
242
|
+
page: 页码. Defaluts to 1
|
|
243
|
+
num: 返回数量. Defaluts to 100
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
list: `Song` 列表
|
|
247
|
+
"""
|
|
248
|
+
if t not in [
|
|
249
|
+
TabType.SONG,
|
|
250
|
+
TabType.COMPOSER,
|
|
251
|
+
TabType.LYRICIST,
|
|
252
|
+
TabType.PRODUCER,
|
|
253
|
+
TabType.MUSICIAN,
|
|
254
|
+
TabType.ARRANGER,
|
|
255
|
+
]:
|
|
256
|
+
raise ValueError(
|
|
257
|
+
"t must be in [TabType.SONG, TabType.COMPOSER, TabType.LYRICIST, TabType.PRODUCER, TabType.MUSICIAN, TabType.ARRANGER]"
|
|
258
|
+
)
|
|
259
|
+
data = await self.get_tab_detail(t, page, num)
|
|
260
|
+
return Song.from_list(data["List"])
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
import asyncio
|
|
1
2
|
from enum import Enum
|
|
2
|
-
from typing import Optional
|
|
3
|
+
from typing import TYPE_CHECKING, Optional
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from .album import Album
|
|
7
|
+
from .singer import Singer
|
|
3
8
|
|
|
4
9
|
from ..exceptions import ArgsException
|
|
5
10
|
from ..utils.common import get_api, parse_song_info, random_string
|
|
@@ -73,20 +78,17 @@ class Song:
|
|
|
73
78
|
self,
|
|
74
79
|
mid: Optional[str] = None,
|
|
75
80
|
id: Optional[int] = None,
|
|
76
|
-
credential: Optional[Credential] = None,
|
|
77
81
|
):
|
|
78
82
|
"""
|
|
79
83
|
Args:
|
|
80
84
|
mid: 歌曲 mid. 歌曲 id 和歌曲 mid 必须提供其中之一
|
|
81
85
|
id: 歌曲 id. 歌曲 id 和歌曲 mid 必须提供其中之一
|
|
82
|
-
credential: Credential 类. Defaluts to None
|
|
83
86
|
"""
|
|
84
87
|
# ID 检查
|
|
85
88
|
if mid is None and id is None:
|
|
86
89
|
raise ArgsException("请至少提供 mid 和 id 中的其中一个参数。")
|
|
87
90
|
self._mid = mid
|
|
88
91
|
self._id = id
|
|
89
|
-
self.credential = Credential() if credential is None else credential
|
|
90
92
|
self._info: Optional[dict] = None
|
|
91
93
|
|
|
92
94
|
@classmethod
|
|
@@ -105,7 +107,20 @@ class Song:
|
|
|
105
107
|
s._info = info
|
|
106
108
|
return s
|
|
107
109
|
|
|
108
|
-
|
|
110
|
+
@classmethod
|
|
111
|
+
def from_list(cls, data: list[dict]) -> list["Song"]:
|
|
112
|
+
"""
|
|
113
|
+
从列表新建 Song
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
data: 歌曲列表
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
list: 歌曲列表
|
|
120
|
+
"""
|
|
121
|
+
return [cls.from_dict(info) for info in data]
|
|
122
|
+
|
|
123
|
+
async def __get_info(self) -> dict:
|
|
109
124
|
"""
|
|
110
125
|
获取歌曲必要信息
|
|
111
126
|
"""
|
|
@@ -114,7 +129,7 @@ class Song:
|
|
|
114
129
|
self._info = (await query_by_mid([self._mid]))[0]
|
|
115
130
|
elif self._id:
|
|
116
131
|
self._info = (await query_by_id([self._id]))[0]
|
|
117
|
-
return self._info
|
|
132
|
+
return self._info # type: ignore
|
|
118
133
|
|
|
119
134
|
@property
|
|
120
135
|
async def mid(self) -> str:
|
|
@@ -140,6 +155,14 @@ class Song:
|
|
|
140
155
|
self._id = (await self.__get_info())["info"]["id"]
|
|
141
156
|
return int(self._id)
|
|
142
157
|
|
|
158
|
+
def __repr__(self) -> str:
|
|
159
|
+
return f"Song(mid={self._mid}, id={self._id})"
|
|
160
|
+
|
|
161
|
+
def __str__(self) -> str:
|
|
162
|
+
if self._info:
|
|
163
|
+
return str(self._info)
|
|
164
|
+
return self.__repr__()
|
|
165
|
+
|
|
143
166
|
async def __prepare_param(self, is_mid: bool = False, is_id: bool = False) -> dict:
|
|
144
167
|
"""
|
|
145
168
|
准备请求参数
|
|
@@ -173,6 +196,28 @@ class Song:
|
|
|
173
196
|
"""
|
|
174
197
|
return (await self.__get_info())["info"]
|
|
175
198
|
|
|
199
|
+
async def get_singer(self) -> "Singer":
|
|
200
|
+
"""
|
|
201
|
+
获取歌曲歌手
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
Singer: 歌手
|
|
205
|
+
"""
|
|
206
|
+
from .singer import Singer
|
|
207
|
+
|
|
208
|
+
return Singer((await self.__get_info())["singer"]["mid"])
|
|
209
|
+
|
|
210
|
+
async def get_album(self) -> "Album":
|
|
211
|
+
"""
|
|
212
|
+
获取歌曲专辑
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
Album: 专辑
|
|
216
|
+
"""
|
|
217
|
+
from .album import Album
|
|
218
|
+
|
|
219
|
+
return Album((await self.__get_info())["album"]["mid"])
|
|
220
|
+
|
|
176
221
|
async def get_detail(self) -> dict:
|
|
177
222
|
"""
|
|
178
223
|
获取歌曲详细信息
|
|
@@ -208,7 +253,7 @@ class Song:
|
|
|
208
253
|
param = await self.__prepare_param(is_id=True)
|
|
209
254
|
return (await Api(**API["labels"]).update_params(**param).result)["labels"]
|
|
210
255
|
|
|
211
|
-
async def
|
|
256
|
+
async def get_related_songlist(self) -> list[dict]:
|
|
212
257
|
"""
|
|
213
258
|
获取歌曲相关歌单
|
|
214
259
|
|
|
@@ -266,6 +311,7 @@ class Song:
|
|
|
266
311
|
self,
|
|
267
312
|
file_type: SongFileType = SongFileType.MP3_128,
|
|
268
313
|
url_type: UrlType = UrlType.PLAY,
|
|
314
|
+
credential: Optional[Credential] = None,
|
|
269
315
|
) -> dict[str, str]:
|
|
270
316
|
"""
|
|
271
317
|
获取歌曲文件链接
|
|
@@ -273,11 +319,12 @@ class Song:
|
|
|
273
319
|
Args:
|
|
274
320
|
file_type: 歌曲文件类型. Defaults to SongFileType.MP3_128
|
|
275
321
|
url_type: 歌曲链接类型. Defaults to UrlType.PLAY
|
|
322
|
+
credential: 账号凭证. Defaults to None
|
|
276
323
|
|
|
277
324
|
Returns:
|
|
278
325
|
dict: 链接字典
|
|
279
326
|
"""
|
|
280
|
-
return await get_song_urls([await self.mid], file_type, url_type)
|
|
327
|
+
return await get_song_urls([await self.mid], file_type, url_type, credential)
|
|
281
328
|
|
|
282
329
|
async def get_file_size(self, file_type: Optional[SongFileType] = None) -> dict:
|
|
283
330
|
"""
|
|
@@ -372,18 +419,25 @@ async def get_song_urls(
|
|
|
372
419
|
)
|
|
373
420
|
api = Api(**API[url_type.value], credential=credential)
|
|
374
421
|
urls = {}
|
|
422
|
+
tasks = []
|
|
375
423
|
for mid in mid_list:
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
"
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
424
|
+
|
|
425
|
+
async def get_song_url(mid):
|
|
426
|
+
# 构造请求参数
|
|
427
|
+
file_name = [f"{file_type.s}{_}{_}{file_type.e}" for _ in mid]
|
|
428
|
+
param = {
|
|
429
|
+
"filename": file_name,
|
|
430
|
+
"guid": random_string(32, "abcdef1234567890"),
|
|
431
|
+
"songmid": mid,
|
|
432
|
+
"songtype": [1 for _ in range(len(mid))],
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
res = await api.update_params(**param).result
|
|
436
|
+
data = res["midurlinfo"]
|
|
437
|
+
for info in data:
|
|
438
|
+
song_url = domain + info["wifiurl"] if info["wifiurl"] else ""
|
|
439
|
+
urls[info["songmid"]] = song_url
|
|
440
|
+
|
|
441
|
+
tasks.append(asyncio.create_task(get_song_url(mid)))
|
|
442
|
+
await asyncio.gather(*tasks)
|
|
389
443
|
return urls
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
from typing import Optional
|
|
2
2
|
|
|
3
|
-
from qqmusic_api.api.song import Song
|
|
4
|
-
|
|
5
3
|
from ..utils.common import get_api
|
|
6
4
|
from ..utils.network import Api
|
|
5
|
+
from .song import Song
|
|
7
6
|
|
|
8
7
|
API = get_api("songlist")
|
|
9
8
|
|
|
@@ -21,6 +20,14 @@ class Songlist:
|
|
|
21
20
|
self.id = id
|
|
22
21
|
self._info: Optional[dict] = None
|
|
23
22
|
|
|
23
|
+
def __repr__(self) -> str:
|
|
24
|
+
return f"Songlist(id={self.id})"
|
|
25
|
+
|
|
26
|
+
def __str__(self) -> str:
|
|
27
|
+
if self._info:
|
|
28
|
+
return str(self._info)
|
|
29
|
+
return self.__repr__()
|
|
30
|
+
|
|
24
31
|
async def __get_info(self):
|
|
25
32
|
if not self._info:
|
|
26
33
|
param = {
|
|
@@ -63,6 +70,7 @@ class Songlist:
|
|
|
63
70
|
async def get_song_tag(self) -> list[dict]:
|
|
64
71
|
"""
|
|
65
72
|
获取歌单歌曲标签
|
|
73
|
+
注:存在几率返回为空
|
|
66
74
|
|
|
67
75
|
Returns:
|
|
68
76
|
list: 歌单歌曲标签
|
|
@@ -70,7 +78,7 @@ class Songlist:
|
|
|
70
78
|
result = await self.__get_info()
|
|
71
79
|
return result["songtag"]
|
|
72
80
|
|
|
73
|
-
async def get_song_mid(self) -> list[
|
|
81
|
+
async def get_song_mid(self) -> list[str]:
|
|
74
82
|
"""
|
|
75
83
|
获取歌单歌曲全部 mid
|
|
76
84
|
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
|
|
1
3
|
from qqmusic_api.api.song import Song
|
|
2
4
|
from qqmusic_api.utils.network import Api
|
|
5
|
+
|
|
3
6
|
from ..utils.common import get_api
|
|
4
|
-
import datetime
|
|
5
7
|
|
|
6
8
|
API = get_api("top")
|
|
7
9
|
|
|
@@ -9,10 +11,8 @@ API = get_api("top")
|
|
|
9
11
|
async def get_top_category(show_detail: bool = False) -> list[dict]:
|
|
10
12
|
"""
|
|
11
13
|
获取所有排行榜
|
|
12
|
-
|
|
13
14
|
Args:
|
|
14
15
|
show_detail: 是否显示详情(包括介绍,前三歌曲). Defaults to False
|
|
15
|
-
|
|
16
16
|
Returns:
|
|
17
17
|
list: 排行榜信息
|
|
18
18
|
"""
|
|
@@ -46,18 +46,23 @@ class Top:
|
|
|
46
46
|
"""
|
|
47
47
|
Args:
|
|
48
48
|
id: 排行榜 ID
|
|
49
|
-
period:
|
|
49
|
+
period: 排行榜时间
|
|
50
50
|
"""
|
|
51
51
|
self.id = id
|
|
52
52
|
self.set_period(period)
|
|
53
53
|
|
|
54
|
+
def __repr__(self) -> str:
|
|
55
|
+
return f"Top(id={self.id}, period={self.period})"
|
|
56
|
+
|
|
57
|
+
def __str__(self) -> str:
|
|
58
|
+
return self.__repr__()
|
|
59
|
+
|
|
54
60
|
def set_period(self, period: str):
|
|
55
61
|
"""
|
|
56
|
-
|
|
62
|
+
设置排行榜时间
|
|
57
63
|
|
|
58
64
|
Args:
|
|
59
|
-
|
|
60
|
-
week: 周
|
|
65
|
+
period: 排行榜周期
|
|
61
66
|
"""
|
|
62
67
|
time_type = "%Y-%m-%d" if self.id in [4, 27, 62] else "'%Y_%W"
|
|
63
68
|
self.period = period or datetime.datetime.strftime(
|
|
@@ -95,4 +100,4 @@ class Top:
|
|
|
95
100
|
"""
|
|
96
101
|
param = {"topId": self.id, "period": self.period, "offset": 0, "num": 100}
|
|
97
102
|
result = await Api(**API["detail"]).update_params(**param).result
|
|
98
|
-
return [Song
|
|
103
|
+
return [Song(id=song["songId"]) for song in result["data"]["song"]]
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"singer_list": {
|
|
3
|
+
"module": "music.musichallSinger.SingerList",
|
|
4
|
+
"method": "GetSingerList",
|
|
5
|
+
"params": {
|
|
6
|
+
"area": "int 地区码",
|
|
7
|
+
"sex": "int 性别",
|
|
8
|
+
"genre": "int 风格",
|
|
9
|
+
"hastag": "int 返回标签"
|
|
10
|
+
},
|
|
11
|
+
"comment": "获取歌手列表"
|
|
12
|
+
},
|
|
13
|
+
"homepage": {
|
|
14
|
+
"module": "music.UnifiedHomepage.UnifiedHomepageSrv",
|
|
15
|
+
"method": "GetHomepageHeader",
|
|
16
|
+
"params": {
|
|
17
|
+
"SingerMid": "str 歌手 mid",
|
|
18
|
+
"IsQueryTabDetail": "int 是否返回 Tab 详细信息",
|
|
19
|
+
"TabID": "int TabID",
|
|
20
|
+
"Order": "int 排序"
|
|
21
|
+
},
|
|
22
|
+
"comment": "获取歌手主页信息"
|
|
23
|
+
},
|
|
24
|
+
"desc": {
|
|
25
|
+
"module": "music.musichallSinger.SingerInfoInter",
|
|
26
|
+
"method": "GetSingerDetail",
|
|
27
|
+
"params": {
|
|
28
|
+
"singer_mids": "list 歌手 mid 列表",
|
|
29
|
+
"group_singer": "int 1",
|
|
30
|
+
"wiki_singer": "int 1"
|
|
31
|
+
},
|
|
32
|
+
"comment": "获取歌手简介"
|
|
33
|
+
},
|
|
34
|
+
"homepage_tab_detail": {
|
|
35
|
+
"module": "music.UnifiedHomepage.UnifiedHomepageSrv",
|
|
36
|
+
"method": "GetHomepageTabDetail",
|
|
37
|
+
"params": {
|
|
38
|
+
"SingerMid": "str 歌手 mid",
|
|
39
|
+
"IsQueryTabDetail": "int 是否返回 Tab 详细信息",
|
|
40
|
+
"TabID": "int TabID",
|
|
41
|
+
"PageNum": "int 页码",
|
|
42
|
+
"PageSize": "int 返回数量",
|
|
43
|
+
"StartIndex": "int 开始位置",
|
|
44
|
+
"Order": "int 排序"
|
|
45
|
+
},
|
|
46
|
+
"comment": "获取歌手首页 Tab 栏信息"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
class ApiException(Exception):
|
|
2
|
+
"""
|
|
3
|
+
API 异常基类
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
def __init__(self, msg: str = "未知原因"):
|
|
7
|
+
super().__init__(msg)
|
|
8
|
+
self.msg = msg
|
|
9
|
+
|
|
10
|
+
def __str__(self):
|
|
11
|
+
return self.msg
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ArgsException(ApiException):
|
|
15
|
+
"""
|
|
16
|
+
参数错误异常
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ClientException(ApiException):
|
|
23
|
+
"""
|
|
24
|
+
服务器连接错误异常
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self):
|
|
28
|
+
super().__init__("连接到服务器时出现了问题")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class NetworkException(ApiException):
|
|
32
|
+
"""
|
|
33
|
+
网络错误异常
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, status: int, msg: str):
|
|
37
|
+
full_msg = f"网络错误,状态码:{status} - {msg}"
|
|
38
|
+
super().__init__(full_msg)
|
|
39
|
+
self.status = status
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ResponseException(ApiException):
|
|
43
|
+
"""
|
|
44
|
+
API 错误异常
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(self, api: list):
|
|
48
|
+
super().__init__()
|
|
49
|
+
self.api = api
|
|
50
|
+
|
|
51
|
+
def __str__(self):
|
|
52
|
+
return f"接口信息:{'.'.join(self.api)}"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class CredentialCanNotRefreshException(ApiException):
|
|
56
|
+
"""
|
|
57
|
+
Crediential 无法刷新异常
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def __init__(self):
|
|
61
|
+
super().__init__("Crediential 无法刷新,请检查是否缺少必要字段")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class CredentialNoMusicidException(ApiException):
|
|
65
|
+
"""
|
|
66
|
+
Crediential 缺少 Musicid 异常
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
def __init__(self):
|
|
70
|
+
super().__init__("Crediential 缺少 Musicid")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class CredentialNoMusickeyException(ApiException):
|
|
74
|
+
"""
|
|
75
|
+
Crediential 缺少 Musickey 异常
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
def __init__(self):
|
|
79
|
+
super().__init__("Crediential 缺少 Musickey")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class LoginDevicesFullException(ApiException):
|
|
83
|
+
"""
|
|
84
|
+
登录设备已满异常
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
def __init__(self):
|
|
88
|
+
super().__init__("登录设备已满")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class AuthcodeExpiredException(ApiException):
|
|
92
|
+
"""
|
|
93
|
+
验证码已失效异常
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
def __init__(self):
|
|
97
|
+
super().__init__("验证码已失效")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class AuthcodeGetFrequentlyException(ApiException):
|
|
101
|
+
"""
|
|
102
|
+
验证码获取频繁异常
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
def __init__(self):
|
|
106
|
+
super().__init__("验证码获取频繁")
|
|
@@ -1,28 +1,29 @@
|
|
|
1
|
+
from dataclasses import asdict, dataclass, field
|
|
2
|
+
from typing import Any, Dict
|
|
3
|
+
|
|
1
4
|
from ..exceptions import (
|
|
5
|
+
CredentialCanNotRefreshException,
|
|
2
6
|
CredentialNoMusicidException,
|
|
3
7
|
CredentialNoMusickeyException,
|
|
4
|
-
CredientialCanNotRefreshException,
|
|
5
8
|
)
|
|
6
9
|
|
|
7
10
|
|
|
11
|
+
@dataclass
|
|
8
12
|
class Credential:
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
self.refresh_key = refresh_key
|
|
15
|
-
self.login_type = 1 if "W_X" in musickey else 2
|
|
13
|
+
musicid: str = ""
|
|
14
|
+
musickey: str = ""
|
|
15
|
+
refresh_key: str = ""
|
|
16
|
+
login_type: int = field(init=False)
|
|
17
|
+
extra_fields: Dict[str, Any] = field(default_factory=dict)
|
|
16
18
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
+
def __post_init__(self):
|
|
20
|
+
self.login_type = 1 if "W_X" in self.musickey else 2
|
|
19
21
|
|
|
20
22
|
def get_dict(self) -> dict:
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
return cookies
|
|
23
|
+
"""
|
|
24
|
+
返回 Credential 的字典表示,包括所有字段。
|
|
25
|
+
"""
|
|
26
|
+
return {**asdict(self), **self.extra_fields}
|
|
26
27
|
|
|
27
28
|
def has_musicid(self) -> bool:
|
|
28
29
|
"""
|
|
@@ -44,10 +45,10 @@ class Credential:
|
|
|
44
45
|
|
|
45
46
|
def raise_for_cannot_refresh(self):
|
|
46
47
|
"""
|
|
47
|
-
无法刷新 Credential
|
|
48
|
+
无法刷新 Credential 时抛出异常
|
|
48
49
|
"""
|
|
49
50
|
if not self.can_refresh():
|
|
50
|
-
raise
|
|
51
|
+
raise CredentialCanNotRefreshException()
|
|
51
52
|
|
|
52
53
|
def raise_for_no_musicid(self):
|
|
53
54
|
"""
|
|
@@ -75,18 +76,18 @@ class Credential:
|
|
|
75
76
|
self.refresh_key = c.refresh_key
|
|
76
77
|
|
|
77
78
|
@classmethod
|
|
78
|
-
def from_cookies(cls, cookies: dict
|
|
79
|
+
def from_cookies(cls, cookies: dict) -> "Credential":
|
|
79
80
|
"""
|
|
80
|
-
从 cookies
|
|
81
|
+
从 cookies 创建 Credential 实例
|
|
81
82
|
|
|
82
83
|
Args:
|
|
83
|
-
cookies : Cookies
|
|
84
|
+
cookies : Cookies 字典.
|
|
84
85
|
|
|
85
86
|
Returns:
|
|
86
|
-
Credential:
|
|
87
|
+
Credential: 凭据类实例
|
|
87
88
|
"""
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
89
|
+
return cls(
|
|
90
|
+
musicid=cookies.get("musicid", ""),
|
|
91
|
+
musickey=cookies.get("musickey", ""),
|
|
92
|
+
refresh_key=cookies.get("refresh_key", ""),
|
|
93
|
+
)
|
|
@@ -6,8 +6,9 @@ from typing import Any
|
|
|
6
6
|
|
|
7
7
|
import aiohttp
|
|
8
8
|
|
|
9
|
+
|
|
9
10
|
from .. import settings
|
|
10
|
-
from ..exceptions import ClientException, NetworkException
|
|
11
|
+
from ..exceptions import ClientException, NetworkException, ResponseException
|
|
11
12
|
from .credential import Credential
|
|
12
13
|
|
|
13
14
|
HEADERS = {
|
|
@@ -78,28 +79,28 @@ class Api:
|
|
|
78
79
|
self.params = {k: "" for k in self.params.keys()}
|
|
79
80
|
self.headers = {k: "" for k in self.headers.keys()}
|
|
80
81
|
self.extra_common = {k: "" for k in self.extra_common.keys()}
|
|
81
|
-
self.__result:
|
|
82
|
+
self.__result: dict | None = None
|
|
82
83
|
|
|
83
84
|
def __setattr__(self, __name: str, __value: Any) -> None:
|
|
84
85
|
"""
|
|
85
86
|
每次更新参数都要把 __result 清除
|
|
86
87
|
"""
|
|
87
|
-
if self.initialized and __name != "
|
|
88
|
+
if self.initialized and __name != "__Api__result":
|
|
88
89
|
self.__result = None
|
|
89
90
|
return super().__setattr__(__name, __value)
|
|
90
91
|
|
|
91
92
|
@property
|
|
92
93
|
def initialized(self):
|
|
93
|
-
return "
|
|
94
|
+
return "__Api__result" in self.__dict__
|
|
94
95
|
|
|
95
96
|
@property
|
|
96
|
-
async def result(self) -> dict
|
|
97
|
+
async def result(self) -> dict:
|
|
97
98
|
"""
|
|
98
99
|
获取请求结果
|
|
99
100
|
"""
|
|
100
101
|
if self.__result is None:
|
|
101
102
|
self.__result = await self.request()
|
|
102
|
-
return self.__result
|
|
103
|
+
return self.__result # type: ignore
|
|
103
104
|
|
|
104
105
|
def update_params(self, **kwargs) -> "Api":
|
|
105
106
|
"""
|
|
@@ -221,15 +222,13 @@ class Api:
|
|
|
221
222
|
except aiohttp.ClientResponseError as e:
|
|
222
223
|
raise NetworkException(e.status, e.message)
|
|
223
224
|
return self.__process_response(resp, await resp.text())
|
|
224
|
-
except aiohttp.
|
|
225
|
+
except aiohttp.ClientConnectionError:
|
|
225
226
|
raise ClientException()
|
|
226
227
|
|
|
227
|
-
def __process_response(
|
|
228
|
-
self, resp: aiohttp.ClientResponse, resp_text: str
|
|
229
|
-
) -> dict | None:
|
|
228
|
+
def __process_response(self, resp: aiohttp.ClientResponse, resp_text: str) -> dict:
|
|
230
229
|
content_length = resp.headers.get("content-length")
|
|
231
230
|
if content_length and int(content_length) == 0:
|
|
232
|
-
|
|
231
|
+
raise ResponseException([self.module, self.method])
|
|
233
232
|
try:
|
|
234
233
|
resp_data = json.loads(resp_text)
|
|
235
234
|
if self.module:
|
|
@@ -237,7 +236,7 @@ class Api:
|
|
|
237
236
|
return request_data["data"]
|
|
238
237
|
return resp_data
|
|
239
238
|
except Exception:
|
|
240
|
-
|
|
239
|
+
raise ResponseException([self.module, self.method])
|
|
241
240
|
|
|
242
241
|
|
|
243
242
|
@atexit.register
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{}
|
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
class ApiException(Exception):
|
|
2
|
-
"""
|
|
3
|
-
API 异常基类
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
def __init__(self, msg: str = "未知原因"):
|
|
7
|
-
super().__init__(msg)
|
|
8
|
-
self.msg = msg
|
|
9
|
-
|
|
10
|
-
def __str__(self):
|
|
11
|
-
return self.msg
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class ArgsException(ApiException):
|
|
15
|
-
"""
|
|
16
|
-
参数错误。
|
|
17
|
-
"""
|
|
18
|
-
|
|
19
|
-
def __init__(self, msg: str):
|
|
20
|
-
super().__init__(msg)
|
|
21
|
-
self.msg = msg
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
class ClientException(ApiException):
|
|
25
|
-
"""
|
|
26
|
-
服务器连接错误
|
|
27
|
-
"""
|
|
28
|
-
|
|
29
|
-
def __init__(self):
|
|
30
|
-
super().__init__()
|
|
31
|
-
self.msg = "连接到服务器时出现了问题"
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
class NetworkException(ApiException):
|
|
35
|
-
"""
|
|
36
|
-
网络错误
|
|
37
|
-
"""
|
|
38
|
-
|
|
39
|
-
def __init__(self, status: int, msg: str):
|
|
40
|
-
super().__init__(msg)
|
|
41
|
-
self.status = status
|
|
42
|
-
self.msg = f"网络错误,状态码:{status} - {msg}"
|
|
43
|
-
|
|
44
|
-
def __str__(self):
|
|
45
|
-
return self.msg
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
class ResponseCodeException(ApiException):
|
|
49
|
-
"""
|
|
50
|
-
API 返回 code 错误
|
|
51
|
-
"""
|
|
52
|
-
|
|
53
|
-
def __init__(self, code: int, subcode: int, api: list):
|
|
54
|
-
super().__init__()
|
|
55
|
-
self.msg = "API 返回 code 错误"
|
|
56
|
-
self.code = code
|
|
57
|
-
self.subcode = subcode
|
|
58
|
-
self.api = api
|
|
59
|
-
|
|
60
|
-
def __str__(self):
|
|
61
|
-
return f"接口信息:{'.'.join(self.api)} 错误代码:{self.code} | {self.subcode}"
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
class CredientialCanNotRefreshException(ApiException):
|
|
65
|
-
"""
|
|
66
|
-
Crediential 无法刷新
|
|
67
|
-
"""
|
|
68
|
-
|
|
69
|
-
def __init__(self):
|
|
70
|
-
super().__init__()
|
|
71
|
-
self.msg = "Crediential 无法刷新,请检查是否缺少必要字段"
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
class CredentialNoMusicidException(ApiException):
|
|
75
|
-
"""
|
|
76
|
-
Crediential 缺少 Musicid
|
|
77
|
-
"""
|
|
78
|
-
|
|
79
|
-
def __init__(self):
|
|
80
|
-
super().__init__()
|
|
81
|
-
self.msg = "Crediential 缺少 Musicid"
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
class CredentialNoMusickeyException(ApiException):
|
|
85
|
-
"""
|
|
86
|
-
Crediential 缺少 Musickey
|
|
87
|
-
"""
|
|
88
|
-
|
|
89
|
-
def __init__(self):
|
|
90
|
-
super().__init__()
|
|
91
|
-
self.msg = "Crediential 缺少 Musickey"
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
class LoginDevicesFullException(ApiException):
|
|
95
|
-
"""
|
|
96
|
-
登录设备已满
|
|
97
|
-
"""
|
|
98
|
-
|
|
99
|
-
def __init__(self):
|
|
100
|
-
super().__init__()
|
|
101
|
-
self.msg = "登录设备已满"
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
class AuthcodeExpiredException(ApiException):
|
|
105
|
-
"""
|
|
106
|
-
验证码已失效
|
|
107
|
-
"""
|
|
108
|
-
|
|
109
|
-
def __init__(self):
|
|
110
|
-
super().__init__()
|
|
111
|
-
self.msg = "验证码已失效"
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
class AuthcodeGetFrequentlyException(ApiException):
|
|
115
|
-
"""
|
|
116
|
-
验证码获取频繁
|
|
117
|
-
"""
|
|
118
|
-
|
|
119
|
-
def __init__(self):
|
|
120
|
-
super().__init__()
|
|
121
|
-
self.msg = "验证码获取频繁"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|