qqmusic-api-python 0.6.9__tar.gz → 0.7.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.
Files changed (211) hide show
  1. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/.github/workflows/docs.yml +6 -6
  2. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/.github/workflows/release.yml +6 -6
  3. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/.github/workflows/testing.yml +2 -2
  4. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/PKG-INFO +1 -2
  5. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/docs/coding.md +78 -65
  6. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/docs/contributing.md +9 -1
  7. qqmusic_api_python-0.7.1/docs/reference/model/helper.md +3 -0
  8. qqmusic_api_python-0.7.1/docs/reference/model/private_message.md +3 -0
  9. qqmusic_api_python-0.7.1/docs/reference/modules/helper.md +3 -0
  10. qqmusic_api_python-0.7.1/docs/reference/modules/helper_utils.md +3 -0
  11. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/docs/release-notes.md +53 -0
  12. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/docs/tutorial/download.md +2 -2
  13. qqmusic_api_python-0.7.1/docs/tutorial/pagination.md +177 -0
  14. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/prek.toml +5 -5
  15. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/pyproject.toml +0 -2
  16. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/qqmusic_api/__init__.py +1 -1
  17. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/qqmusic_api/core/client.py +17 -56
  18. qqmusic_api_python-0.7.1/qqmusic_api/core/pagination.py +464 -0
  19. qqmusic_api_python-0.7.1/qqmusic_api/core/request.py +220 -0
  20. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/qqmusic_api/models/comment.py +18 -0
  21. qqmusic_api_python-0.7.1/qqmusic_api/models/lyric.py +136 -0
  22. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/qqmusic_api/models/request.py +0 -30
  23. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/qqmusic_api/models/search.py +19 -0
  24. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/qqmusic_api/modules/_base.py +34 -160
  25. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/qqmusic_api/modules/album.py +13 -9
  26. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/qqmusic_api/modules/comment.py +101 -62
  27. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/qqmusic_api/modules/helper.py +0 -6
  28. qqmusic_api_python-0.7.1/qqmusic_api/modules/lyric.py +131 -0
  29. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/qqmusic_api/modules/mv.py +7 -5
  30. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/qqmusic_api/modules/private_message.py +17 -17
  31. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/qqmusic_api/modules/recommend.py +23 -37
  32. qqmusic_api_python-0.7.1/qqmusic_api/modules/search.py +289 -0
  33. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/qqmusic_api/modules/singer.py +36 -35
  34. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/qqmusic_api/modules/song.py +67 -34
  35. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/qqmusic_api/modules/songlist.py +8 -9
  36. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/qqmusic_api/modules/top.py +7 -8
  37. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/qqmusic_api/modules/user.py +66 -69
  38. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/qqmusic_api/utils/mqtt.py +4 -2
  39. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/tests/conftest.py +0 -2
  40. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/tests/test_album.py +4 -4
  41. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/tests/test_comment.py +27 -13
  42. qqmusic_api_python-0.7.1/tests/test_lyric.py +70 -0
  43. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/tests/test_mv.py +4 -4
  44. qqmusic_api_python-0.7.1/tests/test_pagination.py +401 -0
  45. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/tests/test_recommend.py +1 -1
  46. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/tests/test_search.py +25 -14
  47. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/tests/test_song.py +35 -12
  48. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/uv.lock +444 -535
  49. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/web/src/app.py +14 -5
  50. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/web/src/core/auth.py +6 -6
  51. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/web/src/core/cache.py +13 -2
  52. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/web/src/core/security.py +9 -3
  53. qqmusic_api_python-0.7.1/web/src/modules/__init__.py +12 -0
  54. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/web/src/modules/comment.py +5 -1
  55. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/web/src/modules/login.py +9 -2
  56. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/web/src/modules/mv.py +3 -0
  57. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/web/src/modules/singer.py +2 -0
  58. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/web/src/modules/song.py +39 -20
  59. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/web/src/modules/songlist.py +3 -0
  60. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/web/src/routes/_helpers.py +11 -1
  61. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/web/src/routes/comment.py +8 -9
  62. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/web/src/routes/login.py +1 -13
  63. qqmusic_api_python-0.7.1/web/src/routes/lyric.py +48 -0
  64. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/web/src/routes/mv.py +0 -3
  65. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/web/src/routes/recommend.py +1 -1
  66. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/web/src/routes/singer.py +0 -2
  67. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/web/src/routes/song.py +26 -17
  68. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/web/src/routes/songlist.py +0 -3
  69. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/web/src/routes/user.py +6 -6
  70. qqmusic_api_python-0.7.1/web/src/routing/adapter_registry.py +56 -0
  71. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/web/src/routing/docstrings.py +7 -1
  72. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/web/src/routing/executor.py +9 -6
  73. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/web/src/routing/params.py +32 -11
  74. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/web/src/routing/route_types.py +3 -2
  75. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/web/src/routing/router_factory.py +42 -8
  76. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/web/tests/test_web_core.py +3 -3
  77. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/web/tests/test_web_docstrings.py +20 -0
  78. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/web/tests/test_web_enums.py +3 -12
  79. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/web/tests/test_web_routes.py +14 -18
  80. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/zensical.toml +7 -1
  81. qqmusic_api_python-0.6.9/.agents/skills/tarsio/SKILL.md +0 -247
  82. qqmusic_api_python-0.6.9/.agents/skills/tarsio/references/api-reference.md +0 -246
  83. qqmusic_api_python-0.6.9/docs/tutorial/pagination.md +0 -112
  84. qqmusic_api_python-0.6.9/qqmusic_api/core/pagination.py +0 -499
  85. qqmusic_api_python-0.6.9/qqmusic_api/core/request.py +0 -150
  86. qqmusic_api_python-0.6.9/qqmusic_api/models/lyric.py +0 -34
  87. qqmusic_api_python-0.6.9/qqmusic_api/modules/lyric.py +0 -50
  88. qqmusic_api_python-0.6.9/qqmusic_api/modules/search.py +0 -181
  89. qqmusic_api_python-0.6.9/tests/test_lyric.py +0 -39
  90. qqmusic_api_python-0.6.9/web/src/modules/__init__.py +0 -1
  91. qqmusic_api_python-0.6.9/web/src/routes/lyric.py +0 -10
  92. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/.agents/skills/pydantic/SKILL.md +0 -0
  93. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/.agents/skills/python-standards/SKILL.md +0 -0
  94. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/.agents/skills/uv-package-manager/SKILL.md +0 -0
  95. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/.dockerignore +0 -0
  96. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/.github/ISSUE_TEMPLATE/bug.yml +0 -0
  97. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  98. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/.github/ISSUE_TEMPLATE/feature.yml +0 -0
  99. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  100. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/.github/renovate.json +0 -0
  101. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/.github/workflows/checking.yaml +0 -0
  102. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/.gitignore +0 -0
  103. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/.markdownlint-cli2.yaml +0 -0
  104. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/AGENTS.md +0 -0
  105. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/CLAUDE.md +0 -0
  106. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/LICENSE +0 -0
  107. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/README.md +0 -0
  108. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/assets/qq-music.svg +0 -0
  109. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/cliff.toml +0 -0
  110. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/docs/index.md +0 -0
  111. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/docs/reference/core/client.md +0 -0
  112. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/docs/reference/core/exception.md +0 -0
  113. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/docs/reference/core/pagination.md +0 -0
  114. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/docs/reference/core/request.md +0 -0
  115. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/docs/reference/core/versioning.md +0 -0
  116. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/docs/reference/model/album.md +0 -0
  117. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/docs/reference/model/base.md +0 -0
  118. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/docs/reference/model/comment.md +0 -0
  119. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/docs/reference/model/login.md +0 -0
  120. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/docs/reference/model/lyric.md +0 -0
  121. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/docs/reference/model/mv.md +0 -0
  122. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/docs/reference/model/recommend.md +0 -0
  123. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/docs/reference/model/request.md +0 -0
  124. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/docs/reference/model/search.md +0 -0
  125. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/docs/reference/model/singer.md +0 -0
  126. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/docs/reference/model/song.md +0 -0
  127. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/docs/reference/model/songlist.md +0 -0
  128. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/docs/reference/model/top.md +0 -0
  129. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/docs/reference/model/user.md +0 -0
  130. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/docs/reference/modules/album.md +0 -0
  131. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/docs/reference/modules/comment.md +0 -0
  132. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/docs/reference/modules/login.md +0 -0
  133. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/docs/reference/modules/login_utils.md +0 -0
  134. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/docs/reference/modules/lyric.md +0 -0
  135. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/docs/reference/modules/mv.md +0 -0
  136. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/docs/reference/modules/private_message.md +0 -0
  137. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/docs/reference/modules/recommend.md +0 -0
  138. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/docs/reference/modules/search.md +0 -0
  139. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/docs/reference/modules/singer.md +0 -0
  140. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/docs/reference/modules/song.md +0 -0
  141. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/docs/reference/modules/songlist.md +0 -0
  142. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/docs/reference/modules/top.md +0 -0
  143. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/docs/reference/modules/user.md +0 -0
  144. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/docs/tutorial/client.md +0 -0
  145. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/docs/tutorial/credential.md +0 -0
  146. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/docs/tutorial/error-handling.md +0 -0
  147. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/docs/tutorial/login.md +0 -0
  148. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/docs/tutorial/start.md +0 -0
  149. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/docs/tutorial/web.md +0 -0
  150. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/examples/download_song.py +0 -0
  151. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/examples/get_all_sheets.py +0 -0
  152. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/examples/phone_login.py +0 -0
  153. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/examples/private_message.py +0 -0
  154. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/examples/qrcode_login.py +0 -0
  155. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/examples/upload_file.py +0 -0
  156. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/qqmusic_api/algorithms/__init__.py +0 -0
  157. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/qqmusic_api/algorithms/sign.py +0 -0
  158. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/qqmusic_api/algorithms/tripledes.py +0 -0
  159. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/qqmusic_api/core/__init__.py +0 -0
  160. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/qqmusic_api/core/exceptions.py +0 -0
  161. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/qqmusic_api/core/versioning.py +0 -0
  162. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/qqmusic_api/models/__init__.py +0 -0
  163. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/qqmusic_api/models/_validator.py +0 -0
  164. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/qqmusic_api/models/album.py +0 -0
  165. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/qqmusic_api/models/base.py +0 -0
  166. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/qqmusic_api/models/helper.py +0 -0
  167. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/qqmusic_api/models/login.py +0 -0
  168. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/qqmusic_api/models/mv.py +0 -0
  169. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/qqmusic_api/models/private_message.py +0 -0
  170. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/qqmusic_api/models/recommend.py +0 -0
  171. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/qqmusic_api/models/singer.py +0 -0
  172. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/qqmusic_api/models/song.py +0 -0
  173. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/qqmusic_api/models/songlist.py +0 -0
  174. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/qqmusic_api/models/top.py +0 -0
  175. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/qqmusic_api/models/user.py +0 -0
  176. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/qqmusic_api/modules/__init__.py +0 -0
  177. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/qqmusic_api/modules/helper_utils.py +0 -0
  178. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/qqmusic_api/modules/login.py +0 -0
  179. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/qqmusic_api/modules/login_utils.py +0 -0
  180. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/qqmusic_api/utils/__init__.py +0 -0
  181. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/qqmusic_api/utils/common.py +0 -0
  182. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/qqmusic_api/utils/device.py +0 -0
  183. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/qqmusic_api/utils/qimei.py +0 -0
  184. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/scripts/ag-1.py +0 -0
  185. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/tests/test_login.py +0 -0
  186. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/tests/test_login_utils.py +0 -0
  187. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/tests/test_private_message.py +0 -0
  188. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/tests/test_singer.py +0 -0
  189. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/tests/test_songlist.py +0 -0
  190. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/tests/test_top.py +0 -0
  191. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/tests/test_user.py +0 -0
  192. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/web/.gitignore +0 -0
  193. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/web/Dockerfile +0 -0
  194. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/web/README.md +0 -0
  195. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/web/accounts.example.toml +0 -0
  196. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/web/config.example.toml +0 -0
  197. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/web/docker-compose.example.yml +0 -0
  198. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/web/requirements.txt +0 -0
  199. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/web/run.py +0 -0
  200. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/web/src/core/__init__.py +0 -0
  201. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/web/src/core/config.py +0 -0
  202. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/web/src/core/credential_store.py +0 -0
  203. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/web/src/core/deps.py +0 -0
  204. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/web/src/core/response.py +0 -0
  205. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/web/src/routes/__init__.py +0 -0
  206. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/web/src/routes/album.py +0 -0
  207. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/web/src/routes/search.py +0 -0
  208. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/web/src/routes/top.py +0 -0
  209. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/web/src/routing/__init__.py +0 -0
  210. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/web/tests/conftest.py +0 -0
  211. {qqmusic_api_python-0.6.9 → qqmusic_api_python-0.7.1}/web/tests/test_web_route_validation.py +0 -0
@@ -19,22 +19,22 @@ jobs:
19
19
  runs-on: ubuntu-latest
20
20
  steps:
21
21
  - name: Checkout
22
- uses: actions/checkout@v6
22
+ uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
23
23
  with:
24
24
  fetch-depth: 0
25
25
  token: ${{ secrets.GH_TOKEN }}
26
26
  - name: Setup Pages
27
27
  id: pages
28
- uses: actions/configure-pages@v6
28
+ uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6
29
29
  - name: Install uv
30
- uses: astral-sh/setup-uv@v7
30
+ uses: astral-sh/setup-uv@94527f2e458b27549849d47d273a16bec83a01e9 # v7
31
31
  with:
32
32
  enable-cache: true
33
33
  python-version: 3.12
34
34
  - name: Install the project
35
35
  run: uv sync --group docs
36
36
  - name: Generate release-notes
37
- uses: orhun/git-cliff-action@v4
37
+ uses: orhun/git-cliff-action@f50e11560dce63f7c33227798f90b924471a88b5 # v4
38
38
  with:
39
39
  config: cliff.toml
40
40
  args: --verbose --strip all
@@ -55,7 +55,7 @@ jobs:
55
55
  - name: Build with zensical
56
56
  run: uv run zensical build
57
57
  - name: Upload artifact
58
- uses: actions/upload-pages-artifact@v5
58
+ uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5
59
59
  with:
60
60
  path: ./site
61
61
 
@@ -68,4 +68,4 @@ jobs:
68
68
  steps:
69
69
  - name: Deploy to GitHub Pages
70
70
  id: deployment
71
- uses: actions/deploy-pages@v5
71
+ uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5
@@ -15,13 +15,13 @@ jobs:
15
15
  outputs:
16
16
  release_body: ${{ steps.git-cliff.outputs.content }}
17
17
  steps:
18
- - uses: actions/checkout@v6
18
+ - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
19
19
  with:
20
20
  fetch-depth: 0
21
21
  token: ${{ secrets.GH_TOKEN }}
22
22
  - name: Generate release body
23
23
  id: git-cliff
24
- uses: orhun/git-cliff-action@v4
24
+ uses: orhun/git-cliff-action@f50e11560dce63f7c33227798f90b924471a88b5 # v4
25
25
  with:
26
26
  args: -vv --latest --strip all
27
27
 
@@ -32,11 +32,11 @@ jobs:
32
32
  permissions:
33
33
  id-token: write
34
34
  steps:
35
- - uses: actions/checkout@v6
35
+ - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
36
36
  with:
37
37
  token: ${{ secrets.GH_TOKEN }}
38
38
  - name: Install uv
39
- uses: astral-sh/setup-uv@v7
39
+ uses: astral-sh/setup-uv@94527f2e458b27549849d47d273a16bec83a01e9 # v7
40
40
  with:
41
41
  enable-cache: true
42
42
  - name: Install the project
@@ -51,11 +51,11 @@ jobs:
51
51
  needs: [generate-release-body, publish]
52
52
  steps:
53
53
  - name: Checkout
54
- uses: actions/checkout@v6
54
+ uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
55
55
  with:
56
56
  token: ${{ secrets.GH_TOKEN }}
57
57
  - name: Release
58
- uses: softprops/action-gh-release@v3
58
+ uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3
59
59
  with:
60
60
  body: ${{ needs.generate-release-body.outputs.release_body }}
61
61
  tag_name: ${{ github.ref_name }}
@@ -10,9 +10,9 @@ jobs:
10
10
  test:
11
11
  runs-on: ubuntu-latest
12
12
  steps:
13
- - uses: actions/checkout@v6
13
+ - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
14
14
  - name: Install uv
15
- uses: astral-sh/setup-uv@v7
15
+ uses: astral-sh/setup-uv@94527f2e458b27549849d47d273a16bec83a01e9 # v7
16
16
  with:
17
17
  enable-cache: true
18
18
  python-version: "3.10"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qqmusic-api-python
3
- Version: 0.6.9
3
+ Version: 0.7.1
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/
@@ -35,7 +35,6 @@ Requires-Dist: niquests[speedups]>=3.18.7
35
35
  Requires-Dist: orjson>=3.10.15
36
36
  Requires-Dist: paho-mqtt>=2.1.0
37
37
  Requires-Dist: pydantic>=2.13.3
38
- Requires-Dist: tarsio>=0.5.1
39
38
  Requires-Dist: typing-extensions>=4.12.2
40
39
  Provides-Extra: upload
41
40
  Requires-Dist: cos-python-sdk-v5>=1.9.44; extra == 'upload'
@@ -15,9 +15,9 @@
15
15
  -> Request
16
16
  -> await request
17
17
  -> Client.execute(request)
18
- -> Client.request_api(...) (根据 request.is_jce 分发改用 JCE 或 JSON 协议)
18
+ -> Client.request_api(...)
19
19
  -> Client._build_result(...)
20
- -> 返回原始 dict / TarsDict 或 Pydantic 模型
20
+ -> 返回原始 dict 或 Pydantic 模型
21
21
  ```
22
22
 
23
23
  ### 批量并发请求
@@ -66,6 +66,7 @@ class FooApi(ApiModule):
66
66
  # qqmusic_api/core/client.py
67
67
  from functools import cached_property
68
68
 
69
+
69
70
  class Client:
70
71
  @cached_property
71
72
  def foo(self) -> "FooApi":
@@ -81,9 +82,9 @@ API 方法返回 `Request` 对象,不直接发起请求。使用 `self._build_
81
82
  def get_detail(self, song_id: int):
82
83
  """获取歌曲详情."""
83
84
  return self._build_request(
84
- module="music.songDetail", # 接口所属模块
85
- method="GetDetail", # 方法名
86
- param={"songid": song_id}, # 业务参数
85
+ module="music.songDetail", # 接口所属模块
86
+ method="GetDetail", # 方法名
87
+ param={"songid": song_id}, # 业务参数
87
88
  )
88
89
  ```
89
90
 
@@ -103,21 +104,20 @@ async def quick_search(self, keyword: str) -> dict[str, Any]:
103
104
 
104
105
  ### `_build_request` 参数说明
105
106
 
106
- | 参数 | 类型 | 说明 |
107
- |------------------|-----------------------------|-----------------------------------------------------------|
108
- | `module` | `str` | 接口所属模块名 |
109
- | `method` | `str` | 方法名 |
110
- | `param` | `dict` | 业务参数 |
111
- | `response_model` | `type[BaseModel]` 或 `None` | 响应模型,为 None 时返回原始 dict |
112
- | `comm` | `dict` 或 `None` | 附加的公共参数 |
113
- | `override_comm` | `bool` | 为 True 时 `comm` 完全替代自动生成的参数;为 False 时合并 |
114
- | `credential` | `Credential` 或 `None` | 覆盖本次请求的凭证 |
115
- | `platform` | `Platform` 或 `None` | 覆盖本次请求的平台 |
116
- | `is_jce` | `bool` | 是否作为 JCE (Tars) 二进制协议发送 |
117
- | `preserve_bool` | `bool` | 是否保留布尔值原样(默认转为 0/1 整型) |
118
- | `sign` | `bool` | 是否对请求进行签名 |
119
- | `pager_meta` | `PagerMeta` 或 `None` | 分页元数据,提供后返回 `PaginatedRequest` |
120
- | `refresh_meta` | `RefreshMeta` 或 `None` | 换一批元数据,提供后返回 `RefreshableRequest` |
107
+ | 参数 | 类型 | 说明 |
108
+ |--------------------|-------------------------------|-------------------------------------------------------------------------------------------------------------|
109
+ | `module` | `str` | 接口所属模块名 |
110
+ | `method` | `str` | 方法名 |
111
+ | `param` | `dict` | 业务参数 |
112
+ | `response_model` | `type[BaseModel]` 或 `None` | 响应模型,为 None 时返回原始 dict |
113
+ | `comm` | `dict` 或 `None` | 附加的公共参数 |
114
+ | `override_comm` | `bool` | 为 True 时 `comm` 完全替代自动生成的参数;为 False 时合并 |
115
+ | `credential` | `Credential` 或 `None` | 覆盖本次请求的凭证 |
116
+ | `platform` | `Platform` 或 `None` | 覆盖本次请求的平台 |
117
+ | `preserve_bool` | `bool` | 是否保留布尔值原样(默认转为 0/1 整型) |
118
+ | `sign` | `bool` | 是否对请求进行签名 |
119
+ | `pager_strategy` | `PagerStrategy` 或 `None` | 分页策略,提供后返回 `PaginatedRequest`;可链式调用 `.with_extractor()` 提升为 `ItemPaginatedRequest` |
120
+ | `refresh_strategy` | `RefresherStrategy` 或 `None` | 换一批策略,提供后返回 `RefreshableRequest`;可链式调用 `.with_extractor()` 提升为 `ItemRefreshableRequest` |
121
121
 
122
122
  ### `client.request` 参数说明
123
123
 
@@ -176,6 +176,17 @@ class MyResponse(Response):
176
176
 
177
177
  `Response` 基类配置了 `frozen=True`(不可变)和 `extra="ignore"`(忽略多余字段)。
178
178
 
179
+ !!! warning "Pydantic 默认值规范"
180
+
181
+ 定义模型时应避免使用 `None` 作为隐式兜底默认值。如果字段可选或为空,应当使用显式的空标量,或通过 `Field(default_factory=...)` 声明:
182
+ ```python
183
+ class Album(Response):
184
+ name: str = ""
185
+ publish_time: str = ""
186
+ # 列表必须使用 default_factory
187
+ singers: list[Singer] = Field(default_factory=list)
188
+ ```
189
+
179
190
  ### JSONPath 字段映射
180
191
 
181
192
  可以通过 `Field(json_schema_extra={"jsonpath": ...})` 声明字段的 JSONPath 映射路径,自动从嵌套响应中提取数据:
@@ -239,14 +250,16 @@ def get_vip_info(self, *, credential: Credential | None = None):
239
250
  > 若接口需要凭证对象的字段来构建请求参数,
240
251
  > 仍可显式调用 `_require_login` 获取凭证对象。
241
252
 
242
- ## 翻页与换一批
253
+ ## 连续翻页与批次刷新
243
254
 
244
255
  ### 连续翻页
245
256
 
246
- 通过 `pager_meta` 声明连续翻页能力,返回的请求对象会暴露 `.paginate()`:
257
+ 通过 `pager_strategy` 声明连续翻页能力,建议配合显示 Generic 标注(形如
258
+ `OffsetStrategy[GetSonglistDetailResponse]`)以确保静态类型检查与类型推断的准确性,并通过 `.with_extractor()`
259
+ 链式调用绑定实体数据项的提取逻辑:
247
260
 
248
261
  ```python
249
- from ..core.pagination import OffsetStrategy, PagerMeta, ResponseAdapter
262
+ from ..core.pagination import OffsetStrategy
250
263
 
251
264
 
252
265
  def get_detail(self, songlist_id: int, num: int = 10, page: int = 1):
@@ -260,23 +273,23 @@ def get_detail(self, songlist_id: int, num: int = 10, page: int = 1):
260
273
  "song_num": num,
261
274
  },
262
275
  response_model=GetSonglistDetailResponse,
263
- pager_meta=PagerMeta(
264
- strategy=OffsetStrategy(offset_key="song_begin", page_size_key="song_num"),
265
- adapter=ResponseAdapter(
266
- has_more_flag="hasmore",
267
- total="total",
268
- count=lambda response: len(response.songs),
269
- ),
276
+ pager_strategy=OffsetStrategy[GetSonglistDetailResponse](
277
+ offset_key="song_begin",
278
+ page_size_key="song_num",
279
+ has_more_extractor=lambda response: bool(response.hasmore),
280
+ total_extractor=lambda response: response.total,
281
+ count_extractor=lambda response: len(response.songs),
270
282
  ),
271
- )
283
+ ).with_extractor(lambda response: response.songs)
272
284
  ```
273
285
 
274
- ### 换一批
286
+ ### 批次刷新 (Batch Refresh)
275
287
 
276
- 通过 `refresh_meta` 声明换一批能力,返回的请求对象会暴露 `.refresh()`:
288
+ 批次刷新(Batch Refresh)是一种针对推荐或关联接口、支持游标复位与防循环重复游标的特殊游标分页,同样通过 `pager_strategy` 声明:
277
289
 
278
290
  ```python
279
- from ..core.pagination import BatchRefreshStrategy, RefreshMeta, ResponseAdapter
291
+ from ..core.pagination import BatchRefreshStrategy
292
+ from ..models.base import MV
280
293
 
281
294
 
282
295
  def get_related_mv(self, songid: int, last_mvid: str | None = None):
@@ -286,14 +299,12 @@ def get_related_mv(self, songid: int, last_mvid: str | None = None):
286
299
  method="GetSongRelatedMv",
287
300
  param={"songid": str(songid), "songtype": 1, "lastmvid": last_mvid or 0},
288
301
  response_model=GetRelatedMvResponse,
289
- refresh_meta=RefreshMeta(
290
- strategy=BatchRefreshStrategy(refresh_key="lastmvid"),
291
- adapter=ResponseAdapter(
292
- has_more_flag="has_more",
293
- cursor=lambda response: response.mv[-1].id if response.mv else None,
294
- ),
302
+ pager_strategy=BatchRefreshStrategy[GetRelatedMvResponse](
303
+ refresh_key="lastmvid",
304
+ cursor_extractor=lambda response: response.mv[-1].id if response.mv else None,
305
+ has_more_extractor=lambda response: bool(response.has_more),
295
306
  ),
296
- )
307
+ ).with_extractor(lambda response: response.mv)
297
308
  ```
298
309
 
299
310
  ### 内置策略速查
@@ -304,26 +315,7 @@ def get_related_mv(self, songid: int, last_mvid: str | None = None):
304
315
  | `OffsetStrategy` | 偏移量滑窗 | `offset_key` + `page_size_key` |
305
316
  | `CursorStrategy` | 响应游标回写 | `cursor_key` |
306
317
  | `MultiFieldContinuationStrategy` | 多字段续翻 | 自定义 `build_next_params` 函数 |
307
- | `BatchRefreshStrategy` | 换一批 | `refresh_key` |
308
-
309
- `pager_meta` 与 `refresh_meta` 不能同时声明。
310
-
311
- ## JCE (Tars) 协议
312
-
313
- 部分接口使用 JCE 二进制协议而非 JSON。通过 `is_jce=True` 启用:
314
-
315
- ```python
316
- def get_something(self):
317
- """获取数据 (JCE 协议)."""
318
- return self._build_request(
319
- module="music.foo.Svc",
320
- method="GetSomething",
321
- param={0: "value"}, # JCE 使用整数 key
322
- is_jce=True,
323
- )
324
- ```
325
-
326
- JCE 协议的响应会自动解码为 `TarsDict`。
318
+ | `BatchRefreshStrategy` | 批次刷新 | `refresh_key` |
327
319
 
328
320
  ## 请求签名
329
321
 
@@ -372,6 +364,20 @@ self._build_request(
372
364
  )
373
365
  ```
374
366
 
367
+ ## 异常处理
368
+
369
+ 在抛出或处理异常时,应使用项目统一的基于领域驱动(DDD)风格的异常类(继承自 `BaseApiException` 或 `ApiException`
370
+ )。在包装底层异常时,必须使用原生异常链(`raise ... from exc`)保留堆栈追踪:
371
+
372
+ ```python
373
+ from ..core.exceptions import ApiDataError
374
+
375
+ try:
376
+ # ...
377
+ except KeyError as e:
378
+ raise ApiDataError("无法解析歌曲信息") from e
379
+ ```
380
+
375
381
  ## 编写测试
376
382
 
377
383
  测试文件放在 `tests/` 下,按模块命名(如 `test_song.py`)。
@@ -388,7 +394,7 @@ from qqmusic_api import Client
388
394
 
389
395
  async def test_query_song(client: Client) -> None:
390
396
  """测试根据 ID 查询歌曲."""
391
- result = await client.song.query_song(["003w2xz20QlUZt"])
397
+ result = await client.song.query_song([SongQueryInfo(mid="003w2xz20QlUZt")])
392
398
  assert result.tracks
393
399
  assert result.tracks[0].name
394
400
  ```
@@ -398,8 +404,15 @@ async def test_query_song(client: Client) -> None:
398
404
  ```python
399
405
  @pytest.mark.parametrize("page", [1, 2])
400
406
  async def test_general_search(client: Client, page: int) -> None:
401
- """测试综合搜索翻页."""
402
- result = await client.search.general_search("周杰伦", page=page)
407
+ """测试综合搜索翻页逻辑."""
408
+ try:
409
+ result = await client.search.general_search("周杰伦", page=page)
410
+ except Exception as e:
411
+ # 示例:优雅处理网络风控或限流 (需根据实际异常类型调整)
412
+ if "limit" in str(e).lower() or "risk" in str(e).lower():
413
+ pytest.skip(f"Triggered rate limit or risk control: {e}")
414
+ raise
415
+
403
416
  assert result.song.items is not None
404
417
  ```
405
418
 
@@ -419,7 +432,7 @@ async def test_get_vip_info(authenticated_client: Client) -> None:
419
432
  ```python
420
433
  async def test_search_paginate(client: Client) -> None:
421
434
  """测试搜索分页."""
422
- pager = client.search.search_by_type("周杰伦", num=5).paginate(limit=2)
435
+ pager = client.search.search_by_type("周杰伦", num=5).pager(limit=2)
423
436
 
424
437
  assert pager.has_more() is True
425
438
  first_page = await pager.next()
@@ -76,11 +76,19 @@ uv run mkdocs serve
76
76
  * Lint 使用 [Ruff](https://docs.astral.sh/ruff/)
77
77
  * 类型检查使用 `pyrefly`
78
78
 
79
+ ## 测试规范
80
+
81
+ * **专注 Modules 层**:测试重心放在 `modules` 层,将其视为黑盒。采用基于数据驱动(`@pytest.mark.parametrize`)的函数式测试方法。
82
+ * **真实网络请求(No Mock)**:禁止 Mock 底层网络请求或核心组装逻辑。必须直接与真实的 QQ 音乐 API 交互。
83
+ * **优雅处理限流**:触发上游 API 风控或频率限制异常时,必须捕获异常并调用 `pytest.skip()` 安全跳过,严禁导致测试失败。
84
+ * **平铺函数写法**:摒弃测试类(`class TestXXX`),强制采用独立纯函数(Flat Functions)。
85
+ * **无需过度清理**:对于获取、查询类操作,验证连通性与模型解析即可。
86
+
79
87
  ## 代码注释
80
88
 
81
89
  * 注释内容包括:模块注释、类注释、函数注释、参数类型注释、返回值注释
82
90
  * 注释风格遵循 [Google-style docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings)
83
- * 测试函数应包含单行中文 docstring(英文标点)
91
+ * 测试用例必须包含**单行中文 docstring**,且内部必须使用**英文标点符号**。
84
92
 
85
93
  ## 文档规范
86
94
 
@@ -0,0 +1,3 @@
1
+ # helper
2
+
3
+ ::: models.helper
@@ -0,0 +1,3 @@
1
+ # private_message
2
+
3
+ ::: models.private_message
@@ -0,0 +1,3 @@
1
+ # HelperApi
2
+
3
+ ::: modules.helper.HelperApi
@@ -0,0 +1,3 @@
1
+ # HelperUtils
2
+
3
+ ::: modules.helper_utils
@@ -1,4 +1,57 @@
1
1
 
2
+ ## [[0.7.0](https://github.com/L-1124/QQMusicApi/compare/v0.6.9..v0.7.0)] - 2026-07-22
3
+
4
+ ### Bug 修复
5
+
6
+ * fix retry import path ([75d0693](https://github.com/L-1124/QQMusicApi/commit/75d069344a07c6713b4a616ed6304ca6dc1b740a)) by [@L-1124](https://github.com/L-1124)
7
+
8
+ ### 功能更新
9
+
10
+ * **(lyric)** 支持指定特殊歌曲类型(song_type)查询歌词 ([1b0aae0](https://github.com/L-1124/QQMusicApi/commit/1b0aae0db3ee6876b3a77b8d1ce3057b4b3c9cd5)) by [@L-1124](https://github.com/L-1124)
11
+ * **(song)** [**breaking**] 重构歌曲查询支持 SongQueryInfo 与 Web POST 批量路由 ([4cbb2c6](https://github.com/L-1124/QQMusicApi/commit/4cbb2c6e993d58db71494ce31809a144bfc33be4)) by [@L-1124](https://github.com/L-1124)
12
+
13
+ ### 贡献者
14
+
15
+ * @L-1124
16
+ * @github-actions[bot]
17
+
18
+ ## [[0.6.9](https://github.com/L-1124/QQMusicApi/compare/v0.6.8..v0.6.9)] - 2026-07-12
19
+
20
+ ### Bug 修复
21
+
22
+ * **(web)** 修复凭据刷新异常捕获过宽与 ETag 缓存机制形同虚设的问题 ([305a063](https://github.com/L-1124/QQMusicApi/commit/305a0637f6261b3de3710c504dc8b22ec0ec52b8)) by [@L-1124](https://github.com/L-1124)
23
+ * **(web)** 修复配置账号由于重置逻辑缺陷被永久标记失效与流式响应并发槽位泄露 ([56f3034](https://github.com/L-1124/QQMusicApi/commit/56f30347a5182380e14febb1b6b1a80debc74ea3)) by [@L-1124](https://github.com/L-1124)
24
+ * **(web)** 避免将 credential 对象盲目混入参数导致 SDK 签名报错 ([fc8b32c](https://github.com/L-1124/QQMusicApi/commit/fc8b32ca1d8d20f16c52f37272798b3688f2848b)) by [@L-1124](https://github.com/L-1124)
25
+ * **(web)** 修复 KeyedLock finalizer 泄漏与 Redis 序列化 Pydantic 模型问题 ([5440a5e](https://github.com/L-1124/QQMusicApi/commit/5440a5e83b1d110d29f4e1b2eab74f5a8f509847)) by [@L-1124](https://github.com/L-1124)
26
+ * **(web)** 修正 songlist 创建与删除路由的 HTTP 方法 ([43c9ce6](https://github.com/L-1124/QQMusicApi/commit/43c9ce6fd0779895afe70a5c7062d7ead0d69957)) by [@L-1124](https://github.com/L-1124)
27
+ * **(web)** 修复凭据刷新锁与限流器的内存泄漏 ([c5ed6a3](https://github.com/L-1124/QQMusicApi/commit/c5ed6a315e7de680b60b63199350fca276781a46)) by [@L-1124](https://github.com/L-1124)
28
+ * **(web)** 修复 search 路由声明中 Any 导致的类型检查错误 ([d1aea06](https://github.com/L-1124/QQMusicApi/commit/d1aea0631aeee866cc8b09d1d6a086b07f24e7a9)) by [@L-1124](https://github.com/L-1124)
29
+ * **(web)** 修复返回 bool 类型时生成的文档和数据格式问题 ([af8550d](https://github.com/L-1124/QQMusicApi/commit/af8550d08038dc8f66be39e28c9d3a905abf7e63)) by [@L-1124](https://github.com/L-1124)
30
+
31
+ ### 功能更新
32
+
33
+ * **(web)** 添加 UserApi 的DisLike和favor api ([b1aa67d](https://github.com/L-1124/QQMusicApi/commit/b1aa67d5e6608d722b337c53274b4d91963a008d)) by [@L-1124](https://github.com/L-1124)
34
+ * 新增新碟上架 / MV 分类 / 收藏专辑 / 收藏歌曲接口与推荐新歌地区筛选 ([520c92b](https://github.com/L-1124/QQMusicApi/commit/520c92bd4d3546a20358c0a6daf47f67af8813d8)) by [@jinzhongjia](https://github.com/jinzhongjia) in [#284](https://github.com/L-1124/QQMusicApi/pull/284)
35
+
36
+ ### 功能重构
37
+
38
+ * **(web)** 收敛配置修改与硬编码路径到配置层 ([0e5950c](https://github.com/L-1124/QQMusicApi/commit/0e5950c9fec28783f081c7d37754de130dcd1950)) by [@L-1124](https://github.com/L-1124)
39
+ * **(web)** 抽取统一的 refresh_and_store 方法消除重复,并补全重试刷新失败时的无效标记逻辑 ([a4a0729](https://github.com/L-1124/QQMusicApi/commit/a4a072984f2dce212885bc240640c9a859d9b0ed)) by [@L-1124](https://github.com/L-1124)
40
+ * **(web)** 移除 songlist 的动态 getattr 分发以恢复类型安全与修复潜在凭据 KeyError ([c1030e1](https://github.com/L-1124/QQMusicApi/commit/c1030e1d867eed8e42957dbf797f548a754796e5)) by [@L-1124](https://github.com/L-1124)
41
+
42
+ ### 性能优化
43
+
44
+ * **(web)** 启用 SQLite WAL 模式并延迟反序列化随机凭据 ([7d01994](https://github.com/L-1124/QQMusicApi/commit/7d019942ae3812fe286d90eb668dc52318ccabe7)) by [@L-1124](https://github.com/L-1124)
45
+ * **(web)** 缓存路由参数解析以避免重复的 SDK 签名内省 ([61cc5a5](https://github.com/L-1124/QQMusicApi/commit/61cc5a5494afb750149d6689e252cb429e9d109d)) by [@L-1124](https://github.com/L-1124)
46
+ * **(web)** 抽取 app 为会话级 fixture 以大幅降低测试耗时 ([6d13b50](https://github.com/L-1124/QQMusicApi/commit/6d13b50ad42c1bf8f56c596a2fd810cadc23208d)) by [@L-1124](https://github.com/L-1124)
47
+ * **(web)** 移除缓存层与响应中不必要的双重序列化 ([bbc865c](https://github.com/L-1124/QQMusicApi/commit/bbc865c8aa52da6a027064510e94c8fcf8d5ab76)) by [@L-1124](https://github.com/L-1124)
48
+
49
+ ### 贡献者
50
+
51
+ * @L-1124
52
+ * @jinzhongjia [#284](https://github.com/L-1124/QQMusicApi/pull/284)
53
+ * @github-actions[bot]
54
+
2
55
  ## [[0.6.8](https://github.com/L-1124/QQMusicApi/compare/v0.6.7..v0.6.8)] - 2026-07-01
3
56
 
4
57
  ### Bug 修复
@@ -34,7 +34,7 @@ asyncio.run(main())
34
34
  通过 `query_song` 获取歌曲信息后,可从 `pay` 字段判断是否需要付费:
35
35
 
36
36
  ```python
37
- song = await client.song.query_song(["003w2xz20QlUZt"])
37
+ song = await client.song.query_song([SongQueryInfo(mid="003w2xz20QlUZt")])
38
38
  track = song.tracks[0]
39
39
 
40
40
  # pay_play: 1=需要付费播放, 0=免费
@@ -72,7 +72,7 @@ for info in urls.data:
72
72
  `query_song` 返回的 `file` 字段包含各音质的文件大小,值为 `0` 表示该音质不存在:
73
73
 
74
74
  ```python
75
- song = await client.song.query_song(["003w2xz20QlUZt"])
75
+ song = await client.song.query_song([SongQueryInfo(mid="003w2xz20QlUZt")])
76
76
  track = song.tracks[0]
77
77
 
78
78
  file = track.file
@@ -0,0 +1,177 @@
1
+ # Pagination
2
+
3
+ QQMusicApi 提供了统一的分页体系:
4
+
5
+ * **`PaginatedRequest`**:具备连续翻页与批次刷新能力的请求描述符。既可直接 `await` 获取首批响应,也可通过 `.pager()`
6
+ 手动按需步进,或使用 `.paginate()`、`.collect()` 和 `async for` 进行流式与批量遍历。
7
+ * **`ItemPaginatedRequest`**:具备数据项提取能力的分页扩展类。除具备通用分页方法外,还通过 `.iter_items()` 与
8
+ `.collect_items()` 实现了跨越页面边界、直接消费单一具体业务元素(如歌曲、专辑等)的功能。
9
+
10
+ ## 1. 单次请求与无状态步进
11
+
12
+ 即使请求具备分页能力,你依然可以像普通请求一样直接 `await` 它,仅拉取单页数据:
13
+
14
+ ```python
15
+ import asyncio
16
+ from qqmusic_api import Client
17
+
18
+
19
+ async def main() -> None:
20
+ async with Client() as client:
21
+ # 仅获取第 1 页数据
22
+ first_page = await client.album.get_new_album(page=1, num=10)
23
+ print(len(first_page.albums))
24
+
25
+
26
+ asyncio.run(main())
27
+ ```
28
+
29
+ 如果你希望配合上一次响应手动构建下一页请求:
30
+
31
+ ```python
32
+ req1 = client.album.get_new_album(page=1, num=10)
33
+ res1 = await req1
34
+
35
+ # 根据上一次响应获取下一次请求的描述符
36
+ req2 = req1.next_request(res1)
37
+ if req2 is not None:
38
+ res2 = await req2
39
+ ```
40
+
41
+ ## 2. 有状态控制器
42
+
43
+ 通过 `.pager()` 可以创建一个有状态的 `AsyncPager` 控制器:
44
+
45
+ ```python
46
+ import asyncio
47
+ from qqmusic_api import Client
48
+
49
+
50
+ async def main() -> None:
51
+ async with Client() as client:
52
+ pager = client.comment.get_hot_comments(102065756, page_size=5).pager(limit=2)
53
+
54
+ while pager.has_more():
55
+ page = await pager.next()
56
+ print(len(page.comments))
57
+
58
+
59
+ asyncio.run(main())
60
+ ```
61
+
62
+ > `has_more()` 只读取当前分页器的内部状态,不会发起网络请求。`next()` 没有更多数据时会抛出 `StopAsyncIteration`。
63
+
64
+ ## 3. 全量收集与条目平铺
65
+
66
+ 如果你希望直接获取多页响应列表,或者直接获取展平后的所有实体数据项(如所有歌曲或专辑)。为防止无休止拉取带来的耗时与风控风险,强烈建议调用时始终设置合理的
67
+ `limit` 参数:
68
+
69
+ ```python
70
+ import asyncio
71
+ from qqmusic_api import Client
72
+
73
+
74
+ async def main() -> None:
75
+ async with Client() as client:
76
+ req = client.singer.get_album_list(mid="0025NhlN2yWrP4")
77
+
78
+ # 收集前 3 页的 Response 响应对象列表
79
+ pages = await req.collect(limit=3)
80
+ print(f"共获取 {len(pages)} 页响应")
81
+
82
+ # 自动跨页展开提取前 25 个专辑实体
83
+ albums = await req.collect_items(limit=25)
84
+ print(f"共收集 {len(albums)} 个专辑实体")
85
+
86
+
87
+ asyncio.run(main())
88
+ ```
89
+
90
+ ## 4. 异步流式迭代
91
+
92
+ ```python
93
+ import asyncio
94
+ from qqmusic_api import Client
95
+
96
+
97
+ async def main() -> None:
98
+ async with Client() as client:
99
+ req = client.search.search_by_type("周杰伦", num=5)
100
+
101
+ # 方式 1:直接迭代对象本身,等价于 paginate(),连续翻页直至尾页
102
+ async for page in req:
103
+ print("当前页歌曲数:", len(page.song))
104
+ break # 演示示例:仅处理一页后退出
105
+
106
+ # 方式 2:显式限制最大翻页数(推荐在生产环境中为循环设置合理的上限)
107
+ async for page in req.paginate(limit=2):
108
+ print("当前页歌曲数:", len(page.song))
109
+
110
+ # 方式 3:跨页条目级别迭代(自动展平为实体)
111
+ async for song in req.iter_items(limit=10):
112
+ print("歌曲名:", song.name)
113
+
114
+
115
+ asyncio.run(main())
116
+ ```
117
+
118
+ ## 5. 批次刷新与单批次步进
119
+
120
+ 部分关联或推荐类接口(如歌曲相关 MV、相似歌曲等)并非按传统的页码(Page)或偏移量(Offset)递增,而是 **按批次(Batch)** 持续更新内容。
121
+
122
+ 对于此类以批次刷新为主、常通过单次触发拉取的场景,推荐使用有状态的分页控制器 `.pager()` 配合 `.first()` 与 `.next()`
123
+ 精准控制每一批次的获取:
124
+
125
+ ```python
126
+ import asyncio
127
+ from qqmusic_api import Client
128
+
129
+
130
+ async def main() -> None:
131
+ async with Client() as client:
132
+ # 1. 实例化分页控制器
133
+ pager = client.song.get_related_mv(1114857).pager()
134
+
135
+ # 2. 首次加载页面时,拉取首批推荐数据
136
+ first_batch = await pager.first()
137
+ print("首批 MV 数量:", len(first_batch.mv))
138
+
139
+ # 3. 按需触发:调用 pager.next() 刷新拉取下一个批次
140
+ if pager.has_more():
141
+ next_batch = await pager.next()
142
+ print("下一批 MV 数量:", len(next_batch.mv))
143
+
144
+
145
+ asyncio.run(main())
146
+ ```
147
+
148
+ 通过 `pager().first()` 与 `pager().next()`,既能在规范的接口契约下享受自动游标维护与防重复终止保护,又能贴合按批次更新的数据消费模式。
149
+
150
+ ## 6. 动态数据项提取
151
+
152
+ 如果你使用的某个 API 返回的请求对象是原生的 `PaginatedRequest`(即 API 层没有预设数据项提取器),你仍然可以通过
153
+ `.with_extractor()` 动态注入一个提取逻辑。这会将请求无缝转换为具备跨页提取能力的 `ItemPaginatedRequest`。
154
+
155
+ 这在处理一些层级较深、或者没有统一结构的响应时非常有用:
156
+
157
+ ```python
158
+ import asyncio
159
+ from qqmusic_api import Client
160
+
161
+
162
+ async def main() -> None:
163
+ async with Client() as client:
164
+ # 这个 API 返回原生的 PaginatedRequest
165
+ req = client.search.general_search("周杰伦")
166
+
167
+ # 动态绑定 extractor
168
+ # 此时 item_req 类型变为 ItemPaginatedRequest
169
+ item_req = req.with_extractor(lambda r: r.song.items if r.song else [])
170
+
171
+ # 现在你可以非常自然地跨页迭代数据项了!
172
+ async for song in item_req.iter_items(limit=10):
173
+ print("提取到的歌曲:", song)
174
+
175
+
176
+ asyncio.run(main())
177
+ ```