qqmusic-api-python 0.3.2__tar.gz → 0.3.4__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.
Files changed (42) hide show
  1. {qqmusic_api_python-0.3.2 → qqmusic_api_python-0.3.4}/PKG-INFO +6 -8
  2. {qqmusic_api_python-0.3.2 → qqmusic_api_python-0.3.4}/README.md +5 -7
  3. {qqmusic_api_python-0.3.2 → qqmusic_api_python-0.3.4}/qqmusic_api/__init__.py +1 -1
  4. {qqmusic_api_python-0.3.2 → qqmusic_api_python-0.3.4}/qqmusic_api/mv.py +1 -1
  5. {qqmusic_api_python-0.3.2 → qqmusic_api_python-0.3.4}/qqmusic_api/singer.py +15 -27
  6. {qqmusic_api_python-0.3.2 → qqmusic_api_python-0.3.4}/qqmusic_api/song.py +4 -1
  7. qqmusic_api_python-0.3.4/qqmusic_api/songlist.py +146 -0
  8. {qqmusic_api_python-0.3.2 → qqmusic_api_python-0.3.4}/qqmusic_api/user.py +12 -12
  9. {qqmusic_api_python-0.3.2 → qqmusic_api_python-0.3.4}/qqmusic_api/utils/credential.py +2 -4
  10. {qqmusic_api_python-0.3.2 → qqmusic_api_python-0.3.4}/qqmusic_api/utils/network.py +14 -6
  11. {qqmusic_api_python-0.3.2 → qqmusic_api_python-0.3.4}/qqmusic_api/utils/qimei.py +2 -2
  12. {qqmusic_api_python-0.3.2 → qqmusic_api_python-0.3.4}/qqmusic_api/utils/session.py +9 -0
  13. {qqmusic_api_python-0.3.2 → qqmusic_api_python-0.3.4}/tests/test_songlist.py +4 -0
  14. {qqmusic_api_python-0.3.2 → qqmusic_api_python-0.3.4}/tests/test_user.py +16 -0
  15. qqmusic_api_python-0.3.4/web/README.md +46 -0
  16. qqmusic_api_python-0.3.2/qqmusic_api/songlist.py +0 -49
  17. {qqmusic_api_python-0.3.2 → qqmusic_api_python-0.3.4}/.gitignore +0 -0
  18. {qqmusic_api_python-0.3.2 → qqmusic_api_python-0.3.4}/LICENSE +0 -0
  19. {qqmusic_api_python-0.3.2 → qqmusic_api_python-0.3.4}/pyproject.toml +0 -0
  20. {qqmusic_api_python-0.3.2 → qqmusic_api_python-0.3.4}/qqmusic_api/album.py +0 -0
  21. {qqmusic_api_python-0.3.2 → qqmusic_api_python-0.3.4}/qqmusic_api/exceptions/__init__.py +0 -0
  22. {qqmusic_api_python-0.3.2 → qqmusic_api_python-0.3.4}/qqmusic_api/exceptions/api_exception.py +0 -0
  23. {qqmusic_api_python-0.3.2 → qqmusic_api_python-0.3.4}/qqmusic_api/login.py +0 -0
  24. {qqmusic_api_python-0.3.2 → qqmusic_api_python-0.3.4}/qqmusic_api/lyric.py +0 -0
  25. {qqmusic_api_python-0.3.2 → qqmusic_api_python-0.3.4}/qqmusic_api/search.py +0 -0
  26. {qqmusic_api_python-0.3.2 → qqmusic_api_python-0.3.4}/qqmusic_api/top.py +0 -0
  27. {qqmusic_api_python-0.3.2 → qqmusic_api_python-0.3.4}/qqmusic_api/utils/__init__.py +0 -0
  28. {qqmusic_api_python-0.3.2 → qqmusic_api_python-0.3.4}/qqmusic_api/utils/common.py +0 -0
  29. {qqmusic_api_python-0.3.2 → qqmusic_api_python-0.3.4}/qqmusic_api/utils/device.py +0 -0
  30. {qqmusic_api_python-0.3.2 → qqmusic_api_python-0.3.4}/qqmusic_api/utils/sign.py +0 -0
  31. {qqmusic_api_python-0.3.2 → qqmusic_api_python-0.3.4}/qqmusic_api/utils/tripledes.py +0 -0
  32. {qqmusic_api_python-0.3.2 → qqmusic_api_python-0.3.4}/tests/test_album.py +0 -0
  33. {qqmusic_api_python-0.3.2 → qqmusic_api_python-0.3.4}/tests/test_login.py +0 -0
  34. {qqmusic_api_python-0.3.2 → qqmusic_api_python-0.3.4}/tests/test_lyric.py +0 -0
  35. {qqmusic_api_python-0.3.2 → qqmusic_api_python-0.3.4}/tests/test_mv.py +0 -0
  36. {qqmusic_api_python-0.3.2 → qqmusic_api_python-0.3.4}/tests/test_qimei.py +0 -0
  37. {qqmusic_api_python-0.3.2 → qqmusic_api_python-0.3.4}/tests/test_search.py +0 -0
  38. {qqmusic_api_python-0.3.2 → qqmusic_api_python-0.3.4}/tests/test_session.py +0 -0
  39. {qqmusic_api_python-0.3.2 → qqmusic_api_python-0.3.4}/tests/test_sign.py +0 -0
  40. {qqmusic_api_python-0.3.2 → qqmusic_api_python-0.3.4}/tests/test_singer.py +0 -0
  41. {qqmusic_api_python-0.3.2 → qqmusic_api_python-0.3.4}/tests/test_song.py +0 -0
  42. {qqmusic_api_python-0.3.2 → qqmusic_api_python-0.3.4}/tests/test_top.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qqmusic-api-python
3
- Version: 0.3.2
3
+ Version: 0.3.4
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
@@ -69,8 +69,10 @@ Description-Content-Type: text/markdown
69
69
 
70
70
  ## 依赖
71
71
 
72
- - [Cryptography](https://cryptography.io/)
73
- - [HTTPX](https://github.com/encode/httpx/)
72
+ - Cryptography
73
+ - HTTPX
74
+ - aiocache
75
+ - orjson
74
76
 
75
77
  ## 快速上手
76
78
 
@@ -96,11 +98,7 @@ async def main():
96
98
  asyncio.run(main())
97
99
  ```
98
100
 
99
- ## Web API
100
- ```
101
- docker build . -t qq-music-api
102
- docker run -d -p 8000:8000 qq-music-api
103
- ```
101
+ ## [Web API](./web/README.md)
104
102
 
105
103
  ## Licence
106
104
 
@@ -39,8 +39,10 @@
39
39
 
40
40
  ## 依赖
41
41
 
42
- - [Cryptography](https://cryptography.io/)
43
- - [HTTPX](https://github.com/encode/httpx/)
42
+ - Cryptography
43
+ - HTTPX
44
+ - aiocache
45
+ - orjson
44
46
 
45
47
  ## 快速上手
46
48
 
@@ -66,11 +68,7 @@ async def main():
66
68
  asyncio.run(main())
67
69
  ```
68
70
 
69
- ## Web API
70
- ```
71
- docker build . -t qq-music-api
72
- docker run -d -p 8000:8000 qq-music-api
73
- ```
71
+ ## [Web API](./web/README.md)
74
72
 
75
73
  ## Licence
76
74
 
@@ -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.2"
7
+ __version__ = "0.3.4"
8
8
 
9
9
  logger = logging.getLogger("qqmusicapi")
10
10
 
@@ -43,7 +43,7 @@ async def get_detail(vids: list[str]):
43
43
  }, NO_PROCESSOR
44
44
 
45
45
 
46
- @api_request("music.stream.MvUrlProxy", "GetMvUrls")
46
+ @api_request("music.stream.MvUrlProxy", "GetMvUrls", exclude_params=["guid"])
47
47
  async def get_mv_urls(vids: list[str]):
48
48
  """获取 MV 播放链接
49
49
 
@@ -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 RequestGroup, api_request
6
+ from .utils.network import NO_PROCESSOR, RequestGroup, api_request
7
7
 
8
8
 
9
9
  class AreaType(Enum):
@@ -133,7 +133,7 @@ async def get_singer_list(
133
133
  )
134
134
 
135
135
 
136
- @api_request("music.musichallSinger.SingerList", "GetSingerListIndex")
136
+ @api_request("music.musichallSinger.SingerList", "GetSingerListIndex", catch_error_code=[104500])
137
137
  async def get_singer_list_index(
138
138
  area: int | AreaType = AreaType.ALL,
139
139
  sex: int | SexType = SexType.ALL,
@@ -164,10 +164,7 @@ async def get_singer_list_index(
164
164
  "index": index,
165
165
  "sin": sin,
166
166
  "cur_page": cur_page,
167
- }, lambda data: cast(
168
- dict[str, Any],
169
- data,
170
- )
167
+ }, NO_PROCESSOR
171
168
 
172
169
 
173
170
  async def get_singer_list_index_all(
@@ -175,7 +172,7 @@ async def get_singer_list_index_all(
175
172
  sex: int | SexType = SexType.ALL,
176
173
  genre: int | GenreType = GenreType.ALL,
177
174
  index: int | IndexType = IndexType.ALL,
178
- ):
175
+ ) -> list[dict[str, Any]]:
179
176
  """获取所有歌手列表
180
177
 
181
178
  Args:
@@ -206,7 +203,7 @@ async def get_singer_list_index_all(
206
203
 
207
204
  for data in await rg.execute():
208
205
  singer_list.extend(data["singerlist"])
209
- return cast(list[dict[str, Any]], singer_list)
206
+ return singer_list
210
207
 
211
208
 
212
209
  @api_request("music.UnifiedHomepage.UnifiedHomepageSrv", "GetHomepageHeader")
@@ -216,7 +213,7 @@ async def get_info(mid: str):
216
213
  Args:
217
214
  mid: 歌手 mid
218
215
  """
219
- return {"SingerMid": mid}, lambda data: data
216
+ return {"SingerMid": mid}, NO_PROCESSOR
220
217
 
221
218
 
222
219
  @api_request("music.UnifiedHomepage.UnifiedHomepageSrv", "GetHomepageTabDetail")
@@ -305,13 +302,10 @@ async def get_songs_list(mid: str, number: int = 10, begin: int = 0):
305
302
  "order": 1,
306
303
  "number": number,
307
304
  "begin": begin,
308
- }, lambda data: cast(
309
- dict[str, Any],
310
- data,
311
- )
305
+ }, NO_PROCESSOR
312
306
 
313
307
 
314
- async def get_songs_list_all(mid: str):
308
+ async def get_songs_list_all(mid: str) -> list[dict[str, Any]]:
315
309
  """获取歌手所有歌曲列表
316
310
 
317
311
  Args:
@@ -332,7 +326,7 @@ async def get_songs_list_all(mid: str):
332
326
  for res in response:
333
327
  songs.extend([song["songInfo"] for song in res["songList"]])
334
328
 
335
- return cast(list[dict[str, Any]], songs)
329
+ return songs
336
330
 
337
331
 
338
332
  @api_request("music.musichallAlbum.AlbumListServer", "GetAlbumList")
@@ -349,13 +343,10 @@ async def get_album_list(mid: str, number: int = 10, begin: int = 0):
349
343
  "order": 1,
350
344
  "number": number,
351
345
  "begin": begin,
352
- }, lambda data: cast(
353
- dict[str, Any],
354
- data,
355
- )
346
+ }, NO_PROCESSOR
356
347
 
357
348
 
358
- async def get_album_list_all(mid: str):
349
+ async def get_album_list_all(mid: str) -> list[dict[str, Any]]:
359
350
  """获取歌手所有专辑列表
360
351
 
361
352
  Args:
@@ -376,7 +367,7 @@ async def get_album_list_all(mid: str):
376
367
  for res in response:
377
368
  albums.extend(res["albumList"])
378
369
 
379
- return cast(list[dict[str, Any]], albums)
370
+ return albums
380
371
 
381
372
 
382
373
  @api_request("MvService.MvInfoProServer", "GetSingerMvList")
@@ -393,13 +384,10 @@ async def get_mv_list(mid: str, number: int = 10, begin: int = 0):
393
384
  "order": 1,
394
385
  "count": number,
395
386
  "start": begin,
396
- }, lambda data: cast(
397
- dict[str, Any],
398
- data,
399
- )
387
+ }, NO_PROCESSOR
400
388
 
401
389
 
402
- async def get_mv_list_all(mid: str):
390
+ async def get_mv_list_all(mid: str) -> list[dict[str, Any]]:
403
391
  """获取歌手所有专辑列表
404
392
 
405
393
  Args:
@@ -420,4 +408,4 @@ async def get_mv_list_all(mid: str):
420
408
  for res in response:
421
409
  mvs.extend(res["list"])
422
410
 
423
- return cast(list[dict[str, Any]], mvs)
411
+ return mvs
@@ -16,7 +16,7 @@ def _get_extract_func(key: str):
16
16
 
17
17
 
18
18
  @api_request("music.trackInfo.UniformRuleCtrl", "CgiGetTrackInfo")
19
- async def query_song(value: list[str] | list[int]):
19
+ async def query_song(value: list[int] | list[str]):
20
20
  """根据 id 或 mid 获取歌曲信息
21
21
 
22
22
  Args:
@@ -114,6 +114,7 @@ class EncryptedSongFileType(BaseSongFileType):
114
114
  async def get_song_urls(
115
115
  mid: list[str],
116
116
  file_type: SongFileType = SongFileType.MP3_128,
117
+ *,
117
118
  credential: Credential | None = None,
118
119
  ) -> dict[str, str]: ...
119
120
 
@@ -122,6 +123,7 @@ async def get_song_urls(
122
123
  async def get_song_urls(
123
124
  mid: list[str],
124
125
  file_type: EncryptedSongFileType,
126
+ *,
125
127
  credential: Credential | None = None,
126
128
  ) -> dict[str, tuple[str, str]]: ...
127
129
 
@@ -129,6 +131,7 @@ async def get_song_urls(
129
131
  async def get_song_urls(
130
132
  mid: list[str],
131
133
  file_type: SongFileType | EncryptedSongFileType = SongFileType.MP3_128,
134
+ *,
132
135
  credential: Credential | None = None,
133
136
  ) -> dict[str, str] | dict[str, tuple[str, str]]:
134
137
  """获取歌曲文件链接
@@ -0,0 +1,146 @@
1
+ """歌单相关 API"""
2
+
3
+ from typing import Any, cast
4
+
5
+ from .utils.credential import Credential
6
+ from .utils.network import RequestGroup, api_request
7
+
8
+
9
+ @api_request("music.srfDissInfo.DissInfo", "CgiGetDiss")
10
+ async def get_detail(
11
+ songlist_id: int,
12
+ dirid: int = 0,
13
+ num: int = 10,
14
+ page: int = 1,
15
+ onlysong: bool = False,
16
+ tag: bool = True,
17
+ userinfo: bool = True,
18
+ ):
19
+ """获取歌单详细信息和歌曲
20
+
21
+ Args:
22
+ songlist_id: 歌单 ID
23
+ dirid: 歌单 dirid
24
+ num: 返回数量
25
+ page: 页码
26
+ onlysong: 是否仅返回歌曲信息(优先级最大)
27
+ tag: 是否返回歌单的标签信息
28
+ userinfo: 是否返回歌单创建者的用户信息
29
+ """
30
+
31
+ def _processsor(data: dict[str, Any]):
32
+ return {
33
+ "dirinfo": data.get("dirinfo", {}),
34
+ "total_song_num": data.get("total_song_num", 0),
35
+ "songlist_size": data.get("songlist_size", 0),
36
+ "songlist": data.get("songlist", []),
37
+ "songtag": data.get("songtag", []),
38
+ "orderlist": data.get("orderlist", []),
39
+ }
40
+
41
+ return {
42
+ "disstid": songlist_id,
43
+ "dirid": dirid,
44
+ "tag": tag,
45
+ "song_begin": num * (page - 1),
46
+ "song_num": num,
47
+ "userinfo": userinfo,
48
+ "orderlist": True,
49
+ "onlysonglist": onlysong,
50
+ }, _processsor
51
+
52
+
53
+ async def get_songlist(
54
+ songlist_id: int,
55
+ dirid: int = 0,
56
+ ) -> list[dict[str, Any]]:
57
+ """获取歌单中所有歌曲列表
58
+
59
+ Args:
60
+ songlist_id: 歌单 ID
61
+ dirid: 歌单 dirid
62
+ """
63
+ response = await get_detail(songlist_id=songlist_id, dirid=dirid, num=100, onlysong=True)
64
+
65
+ total = response["total_song_num"]
66
+ songs = response["songlist"]
67
+ if total <= 100:
68
+ return cast(list[dict[str, Any]], songs)
69
+
70
+ rg = RequestGroup()
71
+ for p, num in enumerate(range(100, total, 100), start=2):
72
+ rg.add_request(get_detail, songlist_id=songlist_id, dirid=dirid, num=100, page=p, onlysong=True)
73
+
74
+ response = await rg.execute()
75
+ for res in response:
76
+ songs.extend(res["songlist"])
77
+
78
+ return songs
79
+
80
+
81
+ @api_request("music.musicasset.PlaylistBaseWrite", "AddPlaylist", verify=True, cacheable=False)
82
+ async def create(dirname: str, *, credential: Credential | None = None):
83
+ """添加歌单, 重名会在名称后面添加时间戳
84
+
85
+ Args:
86
+ dirname: 歌单名称
87
+ credential: 凭证
88
+
89
+ Returns:
90
+ 创建的歌单基本信息
91
+ """
92
+ return {
93
+ "dirName": dirname,
94
+ }, lambda data: cast(dict[str, Any], data["result"])
95
+
96
+
97
+ @api_request("music.musicasset.PlaylistBaseWrite", "DelPlaylist", verify=True, cacheable=False)
98
+ async def delete(dirid: int, *, credential: Credential | None = None):
99
+ """删除歌单
100
+
101
+ Args:
102
+ dirid: 歌单id
103
+ credential: 凭证
104
+
105
+ Returns:
106
+ 是否删除成功若不存在则返回False
107
+ """
108
+ return {
109
+ "dirId": dirid,
110
+ }, lambda data: data["result"]["dirId"] == dirid
111
+
112
+
113
+ @api_request("music.musicasset.PlaylistDetailWrite", "AddSonglist", verify=True, cacheable=False)
114
+ async def add_songs(dirid: int = 1, song_ids: list[int] = [], *, credential: Credential | None = None):
115
+ """添加歌曲到歌单
116
+
117
+ Args:
118
+ dirid: 歌单 dirid
119
+ song_ids: 歌曲 ID 列表
120
+ credential: 凭证
121
+
122
+ Returns:
123
+ 是否添加成功, 歌曲已存在返回False
124
+ """
125
+ return {
126
+ "dirId": dirid,
127
+ "v_songInfo": [{"songType": 0, "songId": songid} for songid in song_ids],
128
+ }, lambda data: bool(data["result"]["updateTime"])
129
+
130
+
131
+ @api_request("music.musicasset.PlaylistDetailWrite", "DelSonglist", verify=True, cacheable=False)
132
+ async def del_songs(dirid: int = 1, song_ids: list[int] = [], *, credential: Credential | None = None):
133
+ """删除歌单歌曲
134
+
135
+ Args:
136
+ dirid: 歌单 dirid
137
+ song_ids: 歌曲 ID 列表
138
+ credential: 凭证
139
+
140
+ Returns:
141
+ 是否删除成功, 歌曲不存在返回False
142
+ """
143
+ return {
144
+ "dirId": dirid,
145
+ "v_songInfo": [{"songType": 0, "songId": songid} for songid in song_ids],
146
+ }, lambda data: bool(data["result"]["updateTime"])
@@ -32,7 +32,7 @@ async def get_musicid(euin: str):
32
32
  )
33
33
 
34
34
 
35
- @api_request("music.UnifiedHomepage.UnifiedHomepageSrv", "GetHomepageHeader")
35
+ @api_request("music.UnifiedHomepage.UnifiedHomepageSrv", "GetHomepageHeader", cacheable=False)
36
36
  async def get_homepage(euin: str, *, credential: Credential | None = None):
37
37
  """获取用户主页信息(包含音乐基因、歌单等)
38
38
 
@@ -49,7 +49,7 @@ async def get_vip_info(*, credential: Credential | None = None):
49
49
  return {}, NO_PROCESSOR
50
50
 
51
51
 
52
- @api_request("music.concern.RelationList", "GetFollowSingerList", verify=True)
52
+ @api_request("music.concern.RelationList", "GetFollowSingerList", verify=True, cacheable=False)
53
53
  async def get_follow_singers(euin: str, page: int = 1, num: int = 10, *, credential: Credential | None = None):
54
54
  """获取关注歌手列表
55
55
 
@@ -65,7 +65,7 @@ async def get_follow_singers(euin: str, page: int = 1, num: int = 10, *, credent
65
65
  }
66
66
 
67
67
 
68
- @api_request("music.concern.RelationList", "GetFansList", verify=True)
68
+ @api_request("music.concern.RelationList", "GetFansList", verify=True, cacheable=False)
69
69
  async def get_fans(euin: str, page: int = 1, num: int = 10, *, credential: Credential | None = None):
70
70
  """获取粉丝列表
71
71
 
@@ -81,7 +81,7 @@ async def get_fans(euin: str, page: int = 1, num: int = 10, *, credential: Crede
81
81
  }
82
82
 
83
83
 
84
- @api_request("music.homepage.Friendship", "GetFriendList", verify=True)
84
+ @api_request("music.homepage.Friendship", "GetFriendList", verify=True, cacheable=False)
85
85
  async def get_friend(page: int = 1, num: int = 10, *, credential: Credential | None = None):
86
86
  """获取好友列表
87
87
 
@@ -93,10 +93,10 @@ async def get_friend(page: int = 1, num: int = 10, *, credential: Credential | N
93
93
  return {
94
94
  "PageSize": num,
95
95
  "Page": page - 1,
96
- }, lambda data: {"total": data.get("Total", 0), "list": data.get("List", [])}
96
+ }, lambda data: {"total": len(data.get("Friends", [])), "list": data.get("Friends", [])}
97
97
 
98
98
 
99
- @api_request("music.concern.RelationList", "GetFollowUserList", verify=True)
99
+ @api_request("music.concern.RelationList", "GetFollowUserList", verify=True, cacheable=False)
100
100
  async def get_follow_user(euin: str, page: int = 1, num: int = 10, *, credential: Credential | None = None):
101
101
  """获取关注用户列表
102
102
 
@@ -112,7 +112,7 @@ async def get_follow_user(euin: str, page: int = 1, num: int = 10, *, credential
112
112
  }
113
113
 
114
114
 
115
- @api_request("music.musicasset.PlaylistBaseRead", "GetPlaylistByUin")
115
+ @api_request("music.musicasset.PlaylistBaseRead", "GetPlaylistByUin", cacheable=False)
116
116
  async def get_created_songlist(uin: str, *, credential: Credential | None = None):
117
117
  """获取创建的歌单
118
118
 
@@ -123,7 +123,7 @@ async def get_created_songlist(uin: str, *, credential: Credential | None = None
123
123
  return {"uin": uin}, lambda data: cast(list[dict[str, Any]], data.get("v_playlist", []))
124
124
 
125
125
 
126
- @api_request("music.srfDissInfo.DissInfo", "CgiGetDiss")
126
+ @api_request("music.srfDissInfo.DissInfo", "CgiGetDiss", cacheable=False)
127
127
  async def get_fav_song(euin: str, page: int = 1, num: int = 10, *, credential: Credential | None = None):
128
128
  """获取收藏歌曲
129
129
 
@@ -156,7 +156,7 @@ async def get_fav_song(euin: str, page: int = 1, num: int = 10, *, credential: C
156
156
  }, _processsor
157
157
 
158
158
 
159
- @api_request("music.musicasset.PlaylistFavRead", "CgiGetPlaylistFavInfo")
159
+ @api_request("music.musicasset.PlaylistFavRead", "CgiGetPlaylistFavInfo", cacheable=False)
160
160
  async def get_fav_songlist(euin: str, page: int = 1, num: int = 10, *, credential: Credential | None = None):
161
161
  """获取收藏歌单
162
162
 
@@ -169,7 +169,7 @@ async def get_fav_songlist(euin: str, page: int = 1, num: int = 10, *, credentia
169
169
  return {"uin": euin, "offset": (page - 1) * num, "size": num}, NO_PROCESSOR
170
170
 
171
171
 
172
- @api_request("music.musicasset.AlbumFavRead", "CgiGetAlbumFavInfo")
172
+ @api_request("music.musicasset.AlbumFavRead", "CgiGetAlbumFavInfo", cacheable=False)
173
173
  async def get_fav_album(euin: str, page: int = 1, num: int = 10, *, credential: Credential | None = None):
174
174
  """获取收藏专辑
175
175
 
@@ -182,7 +182,7 @@ async def get_fav_album(euin: str, page: int = 1, num: int = 10, *, credential:
182
182
  return {"euin": euin, "offset": (page - 1) * num, "size": num}, NO_PROCESSOR
183
183
 
184
184
 
185
- @api_request("music.musicasset.MVFavRead", "getMyFavMV_v2", verify=True)
185
+ @api_request("music.musicasset.MVFavRead", "getMyFavMV_v2", verify=True, cacheable=False)
186
186
  async def get_fav_mv(euin: str, page: int = 1, num: int = 10, *, credential: Credential | None = None):
187
187
  """获取收藏 MV
188
188
 
@@ -195,7 +195,7 @@ async def get_fav_mv(euin: str, page: int = 1, num: int = 10, *, credential: Cre
195
195
  return {"encuin": euin, "pagesize": num, "num": page - 1}, NO_PROCESSOR
196
196
 
197
197
 
198
- @api_request("music.recommend.UserProfileSettingSvr", "GetProfileReport")
198
+ @api_request("music.recommend.UserProfileSettingSvr", "GetProfileReport", cacheable=False)
199
199
  async def get_music_gene(euin: str, *, credential: Credential | None = None):
200
200
  """获取音乐基因数据
201
201
 
@@ -84,16 +84,14 @@ class Credential:
84
84
  if not self.has_musicid() or not self.has_musickey():
85
85
  return False
86
86
  if await self.is_expired():
87
- return bool(self.refresh_key) and bool(self.refresh_token)
87
+ return bool(self.refresh_key)
88
88
  return True
89
89
 
90
90
  async def is_expired(self) -> bool:
91
91
  """判断 credential 是否过期"""
92
92
  if "musickeyCreateTime" in self.extra_fields and "keyExpiresIn" in self.extra_fields:
93
93
  expired_time_stamp = self.extra_fields["musickeyCreateTime"] + self.extra_fields["keyExpiresIn"]
94
- if expired_time_stamp >= time():
95
- return True
96
- return False
94
+ return expired_time_stamp <= time()
97
95
 
98
96
  from ..login import check_expired
99
97
 
@@ -13,7 +13,7 @@ from typing_extensions import override
13
13
  from ..exceptions import CredentialExpiredError, ResponseCodeError, SignInvalidError
14
14
  from .common import calc_md5
15
15
  from .credential import Credential
16
- from .session import get_session
16
+ from .session import Session, get_session
17
17
  from .sign import sign
18
18
 
19
19
  _P = ParamSpec("_P")
@@ -39,6 +39,7 @@ def api_request(
39
39
  cache_ttl: int | None = None,
40
40
  cacheable: bool = True,
41
41
  exclude_params: list[str] = [],
42
+ catch_error_code: list[int] = [],
42
43
  ):
43
44
  """API请求"""
44
45
 
@@ -55,6 +56,7 @@ def api_request(
55
56
  cacheable=cacheable,
56
57
  cache_ttl=cache_ttl,
57
58
  exclude_params=exclude_params,
59
+ catch_error_code=catch_error_code,
58
60
  )
59
61
 
60
62
  return decorator
@@ -80,17 +82,21 @@ class BaseRequest(ABC):
80
82
  verify: bool = False,
81
83
  ignore_code: bool = False,
82
84
  ) -> None:
83
- self.session = get_session()
84
85
  self._common = common or {}
85
86
  self._credential = credential
86
87
  self.verify = verify
87
88
  self.ignore_code = ignore_code
88
89
  self.cache = self.session._cache
89
90
 
91
+ @property
92
+ def session(self) -> Session:
93
+ """获取请求会话"""
94
+ return get_session()
95
+
90
96
  @property
91
97
  def credential(self) -> Credential:
92
98
  """获取请求凭证"""
93
- return self._credential or (self.session).credential or Credential()
99
+ return self._credential or self.session.credential or Credential()
94
100
 
95
101
  @credential.setter
96
102
  def credential(self, value: Credential):
@@ -114,7 +120,7 @@ class BaseRequest(ABC):
114
120
  common = {
115
121
  "cv": config["version_code"],
116
122
  "v": config["version_code"],
117
- "QIMEI36": (self.session).qimei,
123
+ "QIMEI36": self.session.qimei,
118
124
  }
119
125
  common.update(self.COMMON_DEFAULTS)
120
126
  if credential.has_musicid() and credential.has_musickey():
@@ -189,7 +195,7 @@ class ApiRequest(BaseRequest, Generic[_P, _R]):
189
195
  cache_ttl: int | None = None,
190
196
  cacheable: bool = True,
191
197
  exclude_params: list[str] = [],
192
- **kwargs,
198
+ catch_error_code: list[int] = [],
193
199
  ) -> None:
194
200
  super().__init__(common, credential, verify, ignore_code)
195
201
  self.module = module
@@ -201,6 +207,7 @@ class ApiRequest(BaseRequest, Generic[_P, _R]):
201
207
  self.cacheable = cacheable
202
208
  self.cache_ttl = cache_ttl
203
209
  self.exclude_params = exclude_params
210
+ self.catch_error_code = catch_error_code
204
211
 
205
212
  def copy(self) -> "ApiRequest[_P, _R]":
206
213
  """创建当前 ApiRequest 实例的副本"""
@@ -217,6 +224,7 @@ class ApiRequest(BaseRequest, Generic[_P, _R]):
217
224
  cacheable=self.cacheable,
218
225
  cache_ttl=self.cache_ttl,
219
226
  exclude_params=self.exclude_params.copy(),
227
+ catch_error_code=self.catch_error_code,
220
228
  )
221
229
  req.processor = self.processor
222
230
  return req
@@ -273,7 +281,7 @@ class ApiRequest(BaseRequest, Generic[_P, _R]):
273
281
  code,
274
282
  )
275
283
 
276
- if code == 0:
284
+ if code == 0 or code in self.catch_error_code:
277
285
  return
278
286
 
279
287
  if code == 2000:
@@ -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).decode(),
116
116
  }
117
117
 
118
118
 
@@ -158,7 +158,7 @@ def get_qimei(version: str) -> QimeiResult:
158
158
  device.qimei = data["q36"]
159
159
  save_device(device)
160
160
  return QimeiResult(q16=data["q16"], q36=data["q36"])
161
- except Exception:
161
+ except httpx.HTTPError:
162
162
  if device.qimei:
163
163
  return QimeiResult(q16="", q36=device.qimei)
164
164
  logger.exception("获取 QIMEI 失败,使用默认 QIMEI")
@@ -1,6 +1,7 @@
1
1
  """请求 Session 管理"""
2
2
 
3
3
  import contextvars
4
+ import logging
4
5
  from typing import TypedDict
5
6
 
6
7
  import httpx
@@ -11,6 +12,10 @@ from aiocache.serializers import BaseSerializer
11
12
  from .credential import Credential
12
13
  from .qimei import get_qimei
13
14
 
15
+ # 配置日志记录器
16
+ logger = logging.getLogger(__name__)
17
+ _session_counter = 0
18
+
14
19
 
15
20
  class ApiConfig(TypedDict):
16
21
  """API 配置"""
@@ -102,6 +107,7 @@ def get_session() -> Session:
102
107
  """获取当前上下文的 Session"""
103
108
  session = _session_context.get()
104
109
  if session is None:
110
+ logger.info("创建新的默认Session")
105
111
  session = Session()
106
112
  _session_context.set(session)
107
113
  return session
@@ -109,12 +115,15 @@ def get_session() -> Session:
109
115
 
110
116
  def set_session(session: Session) -> None:
111
117
  """设置当前上下文的 Session"""
118
+ logger.info("设置新的Session到上下文")
112
119
  _session_context.set(session)
113
120
 
114
121
 
115
122
  def clear_session() -> None:
116
123
  """清除当前上下文的 Session"""
124
+ logger.info("清除当前上下文的Session")
117
125
  try:
118
126
  _session_context.set(None)
119
127
  except LookupError:
128
+ logger.warning("尝试清除不存在的Session上下文")
120
129
  pass
@@ -12,3 +12,7 @@ async def test_get_detail():
12
12
  tag=True,
13
13
  userinfo=True,
14
14
  )
15
+
16
+
17
+ async def test_get_songlist():
18
+ assert await songlist.get_songlist(songlist_id=9069454203)
@@ -1,5 +1,6 @@
1
1
  import pytest
2
2
 
3
+ from qqmusic_api import songlist
3
4
  from qqmusic_api.exceptions import CredentialExpiredError
4
5
  from qqmusic_api.user import (
5
6
  Credential,
@@ -68,3 +69,18 @@ class TestUserAPI:
68
69
  """测试获取好友列表"""
69
70
  with pytest.raises(CredentialExpiredError):
70
71
  assert await get_friend(page=1, num=10, credential=self.VALID_CREDENTIAL)
72
+
73
+ async def test_create_songlist(self):
74
+ """测试创建歌单"""
75
+ with pytest.raises(CredentialExpiredError):
76
+ result = await songlist.create(dirname="test", credential=self.VALID_CREDENTIAL)
77
+
78
+ if not result or "dirId" not in result or not result["dirId"]:
79
+ pytest.fail(f"创建歌单失败, result 无效: {result}")
80
+
81
+ dir_id = result["dirId"]
82
+ assert await songlist.add_songs(
83
+ dirid=dir_id, song_ids=[438910555, 9063002], credential=self.VALID_CREDENTIAL
84
+ )
85
+ assert await songlist.del_songs(dirid=dir_id, song_ids=[438910555], credential=self.VALID_CREDENTIAL)
86
+ assert await songlist.delete(dirid=dir_id, credential=self.VALID_CREDENTIAL)
@@ -0,0 +1,46 @@
1
+ # Web Port 使用说明
2
+
3
+ ## 1. 安装与运行
4
+
5
+ ### 克隆仓库
6
+
7
+ ```bash
8
+ git clone https://github.com/luren-dc/QQMusicApi
9
+ ```
10
+
11
+ ### 依赖安装
12
+
13
+ ```bash
14
+ uv sync --group web
15
+ ```
16
+
17
+ ### 启动服务
18
+
19
+ ```bash
20
+ uv run uvicorn web.app:app --host 0.0.0.0 --port 8000 --reload
21
+ ```
22
+
23
+ ### Docker
24
+
25
+ ```bash
26
+ docker build . -t qq-music-api
27
+ docker run -d -p 8000:8000 qq-music-api
28
+ ```
29
+
30
+ ## 2. API Endpoint
31
+
32
+ - **请求格式**: `GET /{module}/{func}`
33
+ - **示例**:
34
+ `GET /song/get_detail?id=12345`
35
+
36
+ ## 3. 请求参数规则
37
+
38
+ ### 类型转换规则
39
+
40
+ | 参数类型 | 示例值 | 说明 |
41
+ | ----------- | ------------------------------- | ------------------------------------ |
42
+ | `int` | `count=5` | 整数 |
43
+ | `bool` | `is_vip=true` | `true`/`1`/`yes` 或 `false`/`0`/`no` |
44
+ | `datetime` | `date=2023-10-01T12:34` | ISO 8601 格式 |
45
+ | `list[int]` | `id=1,2,3` | 逗号分隔的字符串 |
46
+ | `Enum` | `type=SongType.HIT`or`type=HIT` | 枚举名或值(见具体模块定义) |
@@ -1,49 +0,0 @@
1
- """歌单相关 API"""
2
-
3
- from typing import Any
4
-
5
- from .utils.network import api_request
6
-
7
-
8
- @api_request("music.srfDissInfo.DissInfo", "CgiGetDiss")
9
- async def get_detail(
10
- songlist_id: int,
11
- dirid: int = 0,
12
- num: int = 10,
13
- page: int = 1,
14
- onlysong: bool = False,
15
- tag: bool = True,
16
- userinfo: bool = True,
17
- ):
18
- """获取歌单详细信息和歌曲
19
-
20
- Args:
21
- songlist_id: 歌单 ID
22
- dirid: 歌单 dirid
23
- num: 返回数量
24
- page: 页码
25
- onlysong: 是否仅返回歌曲信息(优先级最大)
26
- tag: 是否返回歌单的标签信息
27
- userinfo: 是否返回歌单创建者的用户信息
28
- """
29
-
30
- def _processsor(data: dict[str, Any]):
31
- return {
32
- "dirinfo": data.get("dirinfo", {}),
33
- "total_song_num": data.get("total_song_num", 0),
34
- "songlist_size": data.get("songlist_size", 0),
35
- "songlist": data.get("songlist", []),
36
- "songtag": data.get("songtag", []),
37
- "orderlist": data.get("orderlist", []),
38
- }
39
-
40
- return {
41
- "disstid": songlist_id,
42
- "dirid": dirid,
43
- "tag": tag,
44
- "song_begin": num * (page - 1),
45
- "song_num": num,
46
- "userinfo": userinfo,
47
- "orderlist": True,
48
- "onlysonglist": onlysong,
49
- }, _processsor