qqmusic-api-python 0.5.1__tar.gz → 0.5.3__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 (130) hide show
  1. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/.github/workflows/docs.yml +1 -1
  2. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/PKG-INFO +1 -1
  3. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/docs/release-notes.md +33 -1
  4. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/docs/tutorial/pagination.md +28 -1
  5. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/pyproject.toml +3 -0
  6. qqmusic_api_python-0.5.3/qqmusic_api/__init__.py +52 -0
  7. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/qqmusic_api/core/__init__.py +20 -0
  8. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/qqmusic_api/core/exceptions.py +78 -2
  9. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/qqmusic_api/core/pagination.py +17 -1
  10. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/qqmusic_api/core/versioning.py +1 -1
  11. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/qqmusic_api/models/album.py +7 -1
  12. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/qqmusic_api/models/base.py +3 -3
  13. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/qqmusic_api/models/request.py +5 -1
  14. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/qqmusic_api/models/user.py +1 -1
  15. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/qqmusic_api/modules/login.py +253 -103
  16. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/qqmusic_api/modules/login_utils.py +3 -7
  17. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/qqmusic_api/modules/recommend.py +29 -9
  18. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/qqmusic_api/modules/search.py +22 -10
  19. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/qqmusic_api/modules/singer.py +18 -18
  20. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/qqmusic_api/modules/song.py +4 -3
  21. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/qqmusic_api/modules/user.py +1 -1
  22. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/qqmusic_api/utils/common.py +2 -1
  23. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/tests/test_comment.py +11 -5
  24. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/tests/test_login.py +54 -4
  25. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/tests/test_login_utils.py +1 -1
  26. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/tests/test_recommend.py +2 -2
  27. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/tests/test_search.py +15 -12
  28. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/tests/test_singer.py +9 -9
  29. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/tests/test_song.py +2 -2
  30. qqmusic_api_python-0.5.1/qqmusic_api/__init__.py +0 -32
  31. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/.agents/skills/pydantic/SKILL.md +0 -0
  32. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/.agents/skills/python-standards/SKILL.md +0 -0
  33. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/.agents/skills/tarsio/SKILL.md +0 -0
  34. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/.agents/skills/tarsio/references/api-reference.md +0 -0
  35. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/.agents/skills/uv-package-manager/SKILL.md +0 -0
  36. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/.github/ISSUE_TEMPLATE/bug.yml +0 -0
  37. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  38. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/.github/ISSUE_TEMPLATE/feature.yml +0 -0
  39. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  40. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/.github/renovate.json +0 -0
  41. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/.github/workflows/checking.yaml +0 -0
  42. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/.github/workflows/release.yml +0 -0
  43. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/.github/workflows/testing.yml +0 -0
  44. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/.gitignore +0 -0
  45. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/.markdownlint-cli2.yaml +0 -0
  46. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/AGENTS.md +0 -0
  47. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/LICENSE +0 -0
  48. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/README.md +0 -0
  49. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/assets/qq-music.svg +0 -0
  50. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/cliff.toml +0 -0
  51. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/docs/coding.md +0 -0
  52. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/docs/contributing.md +0 -0
  53. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/docs/index.md +0 -0
  54. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/docs/reference/core/client.md +0 -0
  55. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/docs/reference/core/exception.md +0 -0
  56. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/docs/reference/core/pagination.md +0 -0
  57. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/docs/reference/core/request.md +0 -0
  58. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/docs/reference/core/versioning.md +0 -0
  59. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/docs/reference/model/album.md +0 -0
  60. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/docs/reference/model/base.md +0 -0
  61. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/docs/reference/model/comment.md +0 -0
  62. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/docs/reference/model/login.md +0 -0
  63. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/docs/reference/model/lyric.md +0 -0
  64. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/docs/reference/model/mv.md +0 -0
  65. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/docs/reference/model/recommend.md +0 -0
  66. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/docs/reference/model/request.md +0 -0
  67. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/docs/reference/model/search.md +0 -0
  68. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/docs/reference/model/singer.md +0 -0
  69. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/docs/reference/model/song.md +0 -0
  70. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/docs/reference/model/songlist.md +0 -0
  71. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/docs/reference/model/top.md +0 -0
  72. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/docs/reference/model/user.md +0 -0
  73. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/docs/reference/modules/album.md +0 -0
  74. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/docs/reference/modules/comment.md +0 -0
  75. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/docs/reference/modules/login.md +0 -0
  76. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/docs/reference/modules/login_utils.md +0 -0
  77. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/docs/reference/modules/lyric.md +0 -0
  78. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/docs/reference/modules/mv.md +0 -0
  79. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/docs/reference/modules/recommend.md +0 -0
  80. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/docs/reference/modules/search.md +0 -0
  81. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/docs/reference/modules/singer.md +0 -0
  82. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/docs/reference/modules/song.md +0 -0
  83. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/docs/reference/modules/songlist.md +0 -0
  84. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/docs/reference/modules/top.md +0 -0
  85. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/docs/reference/modules/user.md +0 -0
  86. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/docs/tutorial/client.md +0 -0
  87. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/docs/tutorial/credential.md +0 -0
  88. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/docs/tutorial/start.md +0 -0
  89. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/examples/download_song.py +0 -0
  90. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/examples/phone_login.py +0 -0
  91. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/examples/qrcode_login.py +0 -0
  92. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/prek.toml +0 -0
  93. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/qqmusic_api/algorithms/__init__.py +0 -0
  94. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/qqmusic_api/algorithms/sign.py +0 -0
  95. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/qqmusic_api/algorithms/tripledes.py +0 -0
  96. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/qqmusic_api/core/client.py +0 -0
  97. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/qqmusic_api/core/request.py +0 -0
  98. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/qqmusic_api/models/__init__.py +0 -0
  99. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/qqmusic_api/models/comment.py +0 -0
  100. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/qqmusic_api/models/login.py +0 -0
  101. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/qqmusic_api/models/lyric.py +0 -0
  102. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/qqmusic_api/models/mv.py +0 -0
  103. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/qqmusic_api/models/recommend.py +0 -0
  104. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/qqmusic_api/models/search.py +0 -0
  105. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/qqmusic_api/models/singer.py +0 -0
  106. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/qqmusic_api/models/song.py +0 -0
  107. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/qqmusic_api/models/songlist.py +0 -0
  108. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/qqmusic_api/models/top.py +0 -0
  109. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/qqmusic_api/modules/__init__.py +0 -0
  110. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/qqmusic_api/modules/_base.py +0 -0
  111. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/qqmusic_api/modules/album.py +0 -0
  112. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/qqmusic_api/modules/comment.py +0 -0
  113. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/qqmusic_api/modules/lyric.py +0 -0
  114. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/qqmusic_api/modules/mv.py +0 -0
  115. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/qqmusic_api/modules/songlist.py +0 -0
  116. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/qqmusic_api/modules/top.py +0 -0
  117. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/qqmusic_api/utils/__init__.py +0 -0
  118. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/qqmusic_api/utils/device.py +0 -0
  119. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/qqmusic_api/utils/mqtt.py +0 -0
  120. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/qqmusic_api/utils/qimei.py +0 -0
  121. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/scripts/ag-1.py +0 -0
  122. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/tests/conftest.py +0 -0
  123. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/tests/test_album.py +0 -0
  124. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/tests/test_lyric.py +0 -0
  125. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/tests/test_mv.py +0 -0
  126. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/tests/test_songlist.py +0 -0
  127. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/tests/test_top.py +0 -0
  128. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/tests/test_user.py +0 -0
  129. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/uv.lock +0 -0
  130. {qqmusic_api_python-0.5.1 → qqmusic_api_python-0.5.3}/zensical.toml +0 -0
@@ -53,7 +53,7 @@ jobs:
53
53
  - name: Build with zensical
54
54
  run: uv run zensical build
55
55
  - name: Upload artifact
56
- uses: actions/upload-pages-artifact@v4
56
+ uses: actions/upload-pages-artifact@v5
57
57
  with:
58
58
  path: ./site
59
59
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qqmusic-api-python
3
- Version: 0.5.1
3
+ Version: 0.5.3
4
4
  Summary: QQ音乐API封装库
5
5
  Project-URL: documentation, https://l-1124.github.io/QQMusicApi/
6
6
  Project-URL: homepage, https://l-1124.github.io/QQMusicApi/
@@ -1,5 +1,36 @@
1
1
 
2
- ## What's Changed
2
+ ## [[0.5.2](https://github.com/L-1124/QQMusicApi/compare/v0.5.1..v0.5.2)] - 2026-04-18
3
+
4
+ ### Bug 修复
5
+
6
+ * `UserFavSonglistResponse`jsonpath错误 ([2c54ebb](https://github.com/L-1124/QQMusicApi/commit/2c54ebb8d21a8ba02bb5b865a7ff0aa9aadcbc0d)) by [@L-1124](https://github.com/L-1124)
7
+ * `GetAlbumSongResponse`上游返回单个歌曲信息报错 ([adbdc40](https://github.com/L-1124/QQMusicApi/commit/adbdc4069f964125ca2228d36014f64893f3e69a)) by [@L-1124](https://github.com/L-1124)
8
+
9
+ ### 功能更新
10
+
11
+ * **(core)** 为 ResponsePager 添加 next 与 has_more ([0a63a26](https://github.com/L-1124/QQMusicApi/commit/0a63a264433bf37b5854064aa8ef95edba97adab)) by [@L-1124](https://github.com/L-1124)
12
+
13
+ ### 文档更新
14
+
15
+ * **(pagination)** 修正分页示例错误 ([c310450](https://github.com/L-1124/QQMusicApi/commit/c31045021034ce130136a17b458091c456998d53)) by [@L-1124](https://github.com/L-1124)
16
+
17
+ ### 贡献者
18
+
19
+ * @L-1124
20
+ * @renovate[bot] [#240](https://github.com/L-1124/QQMusicApi/pull/240)
21
+ * @github-actions[bot]
22
+
23
+ ## [[0.5.1](https://github.com/L-1124/QQMusicApi/compare/v0.5.0..v0.5.1)] - 2026-04-13
24
+
25
+ ### 功能更新
26
+
27
+ * 获取歌曲文件支持传入歌曲类型 ([a2ad367](https://github.com/L-1124/QQMusicApi/commit/a2ad3675ac56a501f8e08e761e12c68bf6155a98)) by [@L-1124](https://github.com/L-1124)
28
+ * [**breaking**] 重构歌曲文件获取逻辑,支持获取特殊类型的歌曲文件 ([5e31a48](https://github.com/L-1124/QQMusicApi/commit/5e31a4888cf3d4818b3cd59861ffc4d883c4ca1a)) by [@L-1124](https://github.com/L-1124)
29
+ * 接口请求支持更方便的下一页、换一批请求 ([98693f8](https://github.com/L-1124/QQMusicApi/commit/98693f86995df7a62ca705a46643afffb3306e4a)) by [@L-1124](https://github.com/L-1124) in [#235](https://github.com/L-1124/QQMusicApi/pull/235)
30
+
31
+ ### 功能重构
32
+
33
+ * **(login)** 优化 MQTT 登录链路并替换 HTTP 重试实现 ([244e000](https://github.com/L-1124/QQMusicApi/commit/244e000c08dce6beb3ab46c7d266c361515b4b20)) by [@L-1124](https://github.com/L-1124) in [#237](https://github.com/L-1124/QQMusicApi/pull/237)
3
34
 
4
35
  ### 文档更新
5
36
 
@@ -10,6 +41,7 @@
10
41
  ### 贡献者
11
42
 
12
43
  * @L-1124
44
+ * @renovate[bot] [#238](https://github.com/L-1124/QQMusicApi/pull/238)
13
45
  * @github-actions[bot]
14
46
 
15
47
  ## [[0.5.0](https://github.com/L-1124/QQMusicApi/compare/v0.4.1..v0.5.0)] - 2026-04-02
@@ -19,15 +19,42 @@ from qqmusic_api import Client
19
19
  async def main() -> None:
20
20
  async with Client() as client:
21
21
  pager = client.search.search_by_type("周杰伦", num=5).paginate(limit=3)
22
+ page_number = 1
22
23
 
23
24
  async for page in pager:
24
- print(page.nextpage)
25
+ print(f"第 {page_number} 页")
25
26
  print(len(page.song))
27
+ page_number += 1
28
+
29
+ asyncio.run(main())
30
+ ```
31
+
32
+ ## Pager 手动逐页拉取
33
+
34
+ 如果你希望自行控制何时请求下一页, 可以配合 `has_more()` 与 `next()` 使用。`has_more()` 只读取分页器当前状态, 不会主动发起网络请求。
35
+
36
+ > `next()` 与 `async for` 的终止行为一致: 没有更多结果时会抛出 `StopAsyncIteration`。
37
+
38
+ ```python
39
+ import asyncio
40
+
41
+ from qqmusic_api import Client
42
+
43
+
44
+ async def main() -> None:
45
+ async with Client() as client:
46
+ pager = client.comment.get_hot_comments(102065756, page_size=5).paginate(limit=2)
47
+
48
+ while pager.has_more():
49
+ page = await pager.next()
50
+ print(len(page.comments))
26
51
 
27
52
 
28
53
  asyncio.run(main())
29
54
  ```
30
55
 
56
+ 在第一页尚未请求前, 只要分页器未耗尽且未达到 `limit`, `has_more()` 就会返回 `True`。当上游响应已经明确没有下一页, 或者你已经达到 `limit`, `has_more()` 会变为 `False`。
57
+
31
58
  ## Pager 返回值
32
59
 
33
60
  分页器每次迭代返回的,仍然是该接口原本的响应模型。
@@ -103,6 +103,9 @@ asyncio_mode = "auto"
103
103
  line-length = 120
104
104
  extend-exclude = ["docs"]
105
105
 
106
+ [tool.pyright]
107
+ typeCheckingMode = "basic"
108
+
106
109
  [tool.ruff.lint]
107
110
  extend-select = [
108
111
  "A",
@@ -0,0 +1,52 @@
1
+ """QQMusic API 公开入口."""
2
+
3
+ from .core.client import Client
4
+ from .core.exceptions import (
5
+ ApiError,
6
+ HTTPError,
7
+ LoginAccountBannedError,
8
+ LoginAccountRestrictedError,
9
+ LoginApiError,
10
+ LoginAuthCodeError,
11
+ LoginAuthFailedError,
12
+ LoginBindRequiredError,
13
+ LoginCredentialExpiredError,
14
+ LoginDeviceLimitError,
15
+ LoginError,
16
+ LoginExpiredError,
17
+ LoginRateLimitedError,
18
+ LoginSecurityRequiredError,
19
+ NetworkError,
20
+ NotLoginError,
21
+ RatelimitedError,
22
+ SignInvalidError,
23
+ )
24
+ from .core.versioning import Platform
25
+ from .models.request import Credential
26
+
27
+ __version__ = "0.5.3"
28
+
29
+ __all__ = [
30
+ "ApiError",
31
+ "Client",
32
+ "Credential",
33
+ "HTTPError",
34
+ "LoginAccountBannedError",
35
+ "LoginAccountRestrictedError",
36
+ "LoginApiError",
37
+ "LoginAuthCodeError",
38
+ "LoginAuthFailedError",
39
+ "LoginBindRequiredError",
40
+ "LoginCredentialExpiredError",
41
+ "LoginDeviceLimitError",
42
+ "LoginError",
43
+ "LoginExpiredError",
44
+ "LoginRateLimitedError",
45
+ "LoginSecurityRequiredError",
46
+ "NetworkError",
47
+ "NotLoginError",
48
+ "Platform",
49
+ "RatelimitedError",
50
+ "SignInvalidError",
51
+ "__version__",
52
+ ]
@@ -7,8 +7,18 @@ from .exceptions import (
7
7
  BaseError,
8
8
  CredentialError,
9
9
  HTTPError,
10
+ LoginAccountBannedError,
11
+ LoginAccountRestrictedError,
12
+ LoginApiError,
13
+ LoginAuthCodeError,
14
+ LoginAuthFailedError,
15
+ LoginBindRequiredError,
16
+ LoginCredentialExpiredError,
17
+ LoginDeviceLimitError,
10
18
  LoginError,
11
19
  LoginExpiredError,
20
+ LoginRateLimitedError,
21
+ LoginSecurityRequiredError,
12
22
  NetworkError,
13
23
  NotLoginError,
14
24
  RatelimitedError,
@@ -29,8 +39,18 @@ __all__ = [
29
39
  "ClientConfig",
30
40
  "CredentialError",
31
41
  "HTTPError",
42
+ "LoginAccountBannedError",
43
+ "LoginAccountRestrictedError",
44
+ "LoginApiError",
45
+ "LoginAuthCodeError",
46
+ "LoginAuthFailedError",
47
+ "LoginBindRequiredError",
48
+ "LoginCredentialExpiredError",
49
+ "LoginDeviceLimitError",
32
50
  "LoginError",
33
51
  "LoginExpiredError",
52
+ "LoginRateLimitedError",
53
+ "LoginSecurityRequiredError",
34
54
  "NetworkError",
35
55
  "NotLoginError",
36
56
  "Platform",
@@ -8,8 +8,18 @@ __all__ = [
8
8
  "BaseError",
9
9
  "CredentialError",
10
10
  "HTTPError",
11
+ "LoginAccountBannedError",
12
+ "LoginAccountRestrictedError",
13
+ "LoginApiError",
14
+ "LoginAuthCodeError",
15
+ "LoginAuthFailedError",
16
+ "LoginBindRequiredError",
17
+ "LoginCredentialExpiredError",
18
+ "LoginDeviceLimitError",
11
19
  "LoginError",
12
20
  "LoginExpiredError",
21
+ "LoginRateLimitedError",
22
+ "LoginSecurityRequiredError",
13
23
  "NetworkError",
14
24
  "NotLoginError",
15
25
  "RatelimitedError",
@@ -166,8 +176,74 @@ class LoginError(BaseError):
166
176
  通常在扫码登录流程中断、超时或网络失败时抛出。
167
177
  """
168
178
 
169
- def __init__(self, message: str = "登录失败", cause: BaseException | None = None):
170
- super().__init__(message, cause=cause)
179
+ def __init__(
180
+ self,
181
+ message: str = "登录失败",
182
+ cause: BaseException | None = None,
183
+ context: dict[str, Any] | None = None,
184
+ ):
185
+ super().__init__(message, context=context, cause=cause)
186
+
187
+
188
+ class LoginApiError(LoginError):
189
+ """登录接口返回的业务错误基类."""
190
+
191
+ def __init__(
192
+ self,
193
+ message: str,
194
+ *,
195
+ code: int,
196
+ data: dict[str, Any] | None = None,
197
+ action_url: str = "",
198
+ cause: BaseException | None = None,
199
+ context: dict[str, Any] | None = None,
200
+ ):
201
+ merged_context = dict(context or {})
202
+ merged_context["code"] = code
203
+ if data:
204
+ merged_context["data"] = data
205
+ if action_url:
206
+ merged_context["url"] = action_url
207
+ super().__init__(message, context=merged_context, cause=cause)
208
+ self.code = code
209
+ self.data = data or {}
210
+ self.action_url = action_url
211
+
212
+
213
+ class LoginCredentialExpiredError(LoginApiError):
214
+ """登录刷新凭证不可用或登录态过期."""
215
+
216
+
217
+ class LoginAuthFailedError(LoginApiError):
218
+ """登录认证失败或上游票据不可用."""
219
+
220
+
221
+ class LoginSecurityRequiredError(LoginApiError):
222
+ """登录需要安全验证或人工交互."""
223
+
224
+
225
+ class LoginAccountRestrictedError(LoginApiError):
226
+ """账号登录受限."""
227
+
228
+
229
+ class LoginAccountBannedError(LoginApiError):
230
+ """账号被封禁导致无法登录."""
231
+
232
+
233
+ class LoginBindRequiredError(LoginApiError):
234
+ """登录需要完成账号绑定或设备确认."""
235
+
236
+
237
+ class LoginDeviceLimitError(LoginApiError):
238
+ """登录设备数量超限."""
239
+
240
+
241
+ class LoginAuthCodeError(LoginApiError):
242
+ """手机验证码错误或已失效."""
243
+
244
+
245
+ class LoginRateLimitedError(LoginApiError):
246
+ """登录操作触发频率限制."""
171
247
 
172
248
 
173
249
  class RequestGroupResultMissingError(ApiError):
@@ -434,18 +434,34 @@ class ResponsePager(_BaseResponseAdvancer[RequestResultT], AsyncIterator[Request
434
434
  self._limit = limit
435
435
  self._yielded_count = 0
436
436
 
437
+ def _can_advance(self) -> bool:
438
+ """返回当前分页器是否还能继续产出下一页."""
439
+ if self._next_request is None:
440
+ return False
441
+ if self._limit is None:
442
+ return True
443
+ return self._yielded_count < self._limit
444
+
437
445
  def __aiter__(self) -> AsyncIterator[RequestResultT]:
438
446
  """返回分页器自身, 以支持 `async for` 迭代."""
439
447
  return self
440
448
 
441
449
  async def __anext__(self) -> RequestResultT:
442
450
  """获取并返回下一页响应."""
443
- if self._limit is not None and self._yielded_count >= self._limit:
451
+ if not self._can_advance():
444
452
  raise StopAsyncIteration
445
453
  response = await self._advance()
446
454
  self._yielded_count += 1
447
455
  return response
448
456
 
457
+ async def next(self) -> RequestResultT:
458
+ """获取并返回下一页响应."""
459
+ return await self.__anext__()
460
+
461
+ def has_more(self) -> bool:
462
+ """返回当前分页器是否还能继续产出下一页."""
463
+ return self._can_advance()
464
+
449
465
  def _get_meta(self, request: "PaginatedRequest[RequestResultT]") -> PagerMeta:
450
466
  """读取分页请求对应的连续翻页元数据."""
451
467
  return request.get_pager_meta()
@@ -30,7 +30,7 @@ class VersionProfile:
30
30
  qimei_sdk_version: str | None = None
31
31
 
32
32
 
33
- @dataclass(frozen=True, slots=True)
33
+ @dataclass(slots=True)
34
34
  class VersionPolicy:
35
35
  """请求版本策略."""
36
36
 
@@ -1,6 +1,6 @@
1
1
  """Album API 返回模型定义."""
2
2
 
3
- from pydantic import Field
3
+ from pydantic import Field, field_validator
4
4
 
5
5
  from .base import Album, Singer, Song
6
6
  from .request import Response
@@ -70,3 +70,9 @@ class GetAlbumSongResponse(Response):
70
70
  album_mid: str = Field(alias="albumMid")
71
71
  total_num: int = Field(alias="totalNum")
72
72
  song_list: list[Song] = Field(default_factory=list, json_schema_extra={"jsonpath": "$.songList[*].songInfo"})
73
+
74
+ @field_validator("song_list", mode="before")
75
+ @classmethod
76
+ def _coerce_song_list(cls, value: list[dict] | dict) -> list[dict]:
77
+ """将上游返回的单个歌曲信息统一规整为列表."""
78
+ return [value] if isinstance(value, dict) else value
@@ -19,7 +19,7 @@ _PHOTO_NEW_SIZE_SEGMENTS: Final[dict[int, str]] = {
19
19
 
20
20
 
21
21
  def _normalize_cover_size(size: CoverSize) -> str:
22
- """返回 APK `photo_new` 模板使用的尺寸片段."""
22
+ """返回 `photo_new` 模板使用的尺寸片段."""
23
23
  try:
24
24
  return _PHOTO_NEW_SIZE_SEGMENTS[size]
25
25
  except KeyError as exc:
@@ -27,7 +27,7 @@ def _normalize_cover_size(size: CoverSize) -> str:
27
27
 
28
28
 
29
29
  def _build_photo_new_cover_url(kind: str, mid: str, size: CoverSize) -> str:
30
- """按 APK `photo_new` 规则拼接封面链接."""
30
+ """按 `photo_new` 规则拼接封面链接."""
31
31
  normalized_mid = mid.strip()
32
32
  if not normalized_mid:
33
33
  return ""
@@ -200,7 +200,7 @@ class SongList(Response):
200
200
  picurl: str = Field(default="", validation_alias=AliasChoices("picurl", "cover", "logo", "picUrl"))
201
201
  desc: str = Field(default="", validation_alias=AliasChoices("desc", "description"))
202
202
  songnum: int = Field(default=0, validation_alias=AliasChoices("songnum", "songNum", "song_cnt"))
203
- listennum: int | str = Field(default=0, validation_alias=AliasChoices("listennum", "playCnt", "play_cnt"))
203
+ listennum: int = Field(default=0, validation_alias=AliasChoices("listennum", "playCnt", "play_cnt"))
204
204
 
205
205
 
206
206
  class Song(Response):
@@ -53,6 +53,10 @@ class CommonParams(BaseModel):
53
53
  format: str | None = Field(default=None)
54
54
  in_charset: str | None = Field(default=None, alias="inCharset")
55
55
  out_charset: str | None = Field(default=None, alias="outCharset")
56
+ # [Web] 通知开关
57
+ notice: int | None = Field(default=None)
58
+ # [Web] 新设备码开关
59
+ need_new_code: int | None = Field(default=None)
56
60
 
57
61
 
58
62
  class Credential(BaseModel):
@@ -95,7 +99,7 @@ class Credential(BaseModel):
95
99
  if not isinstance(data, dict):
96
100
  return data
97
101
 
98
- if data.get("loginType") or data.get("login_type"):
102
+ if "loginType" in data or "login_type" in data:
99
103
  return data
100
104
 
101
105
  musickey = data.get("musickey", "")
@@ -126,7 +126,7 @@ class UserFavSonglistResponse(Response):
126
126
  total: int
127
127
  hasmore: int
128
128
  hide: bool
129
- playlists: list[UserFavSonglistItem] = Field(json_schema_extra={"jsonpath": "$.v_list[*]"})
129
+ playlists: list[UserFavSonglistItem] = Field(json_schema_extra={"jsonpath": "$.v_list"})
130
130
  deleted_ids: list[int] = Field(alias="v_delTids")
131
131
  failed_ids: list[int] = Field(alias="v_failTids")
132
132