qqmusic-api-python 0.6.2__tar.gz → 0.6.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 (184) hide show
  1. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/PKG-INFO +1 -1
  2. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/docs/release-notes.md +25 -0
  3. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/qqmusic_api/__init__.py +1 -1
  4. qqmusic_api_python-0.6.3/qqmusic_api/models/_validator.py +30 -0
  5. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/qqmusic_api/models/album.py +1 -7
  6. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/qqmusic_api/models/private_message.py +5 -10
  7. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/qqmusic_api/models/request.py +8 -3
  8. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/qqmusic_api/models/singer.py +33 -78
  9. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/qqmusic_api/models/top.py +7 -10
  10. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/qqmusic_api/models/user.py +81 -20
  11. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/tests/conftest.py +17 -12
  12. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/tests/test_album.py +6 -6
  13. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/tests/test_user.py +21 -2
  14. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/.agents/skills/pydantic/SKILL.md +0 -0
  15. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/.agents/skills/python-standards/SKILL.md +0 -0
  16. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/.agents/skills/tarsio/SKILL.md +0 -0
  17. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/.agents/skills/tarsio/references/api-reference.md +0 -0
  18. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/.agents/skills/uv-package-manager/SKILL.md +0 -0
  19. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/.dockerignore +0 -0
  20. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/.github/ISSUE_TEMPLATE/bug.yml +0 -0
  21. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  22. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/.github/ISSUE_TEMPLATE/feature.yml +0 -0
  23. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  24. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/.github/renovate.json +0 -0
  25. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/.github/workflows/checking.yaml +0 -0
  26. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/.github/workflows/docs.yml +0 -0
  27. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/.github/workflows/release.yml +0 -0
  28. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/.github/workflows/testing.yml +0 -0
  29. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/.gitignore +0 -0
  30. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/.markdownlint-cli2.yaml +0 -0
  31. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/AGENTS.md +0 -0
  32. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/CLAUDE.md +0 -0
  33. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/LICENSE +0 -0
  34. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/README.md +0 -0
  35. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/assets/qq-music.svg +0 -0
  36. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/cliff.toml +0 -0
  37. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/docs/coding.md +0 -0
  38. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/docs/contributing.md +0 -0
  39. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/docs/index.md +0 -0
  40. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/docs/reference/core/client.md +0 -0
  41. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/docs/reference/core/exception.md +0 -0
  42. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/docs/reference/core/pagination.md +0 -0
  43. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/docs/reference/core/request.md +0 -0
  44. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/docs/reference/core/versioning.md +0 -0
  45. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/docs/reference/model/album.md +0 -0
  46. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/docs/reference/model/base.md +0 -0
  47. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/docs/reference/model/comment.md +0 -0
  48. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/docs/reference/model/login.md +0 -0
  49. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/docs/reference/model/lyric.md +0 -0
  50. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/docs/reference/model/mv.md +0 -0
  51. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/docs/reference/model/recommend.md +0 -0
  52. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/docs/reference/model/request.md +0 -0
  53. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/docs/reference/model/search.md +0 -0
  54. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/docs/reference/model/singer.md +0 -0
  55. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/docs/reference/model/song.md +0 -0
  56. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/docs/reference/model/songlist.md +0 -0
  57. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/docs/reference/model/top.md +0 -0
  58. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/docs/reference/model/user.md +0 -0
  59. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/docs/reference/modules/album.md +0 -0
  60. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/docs/reference/modules/comment.md +0 -0
  61. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/docs/reference/modules/login.md +0 -0
  62. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/docs/reference/modules/login_utils.md +0 -0
  63. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/docs/reference/modules/lyric.md +0 -0
  64. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/docs/reference/modules/mv.md +0 -0
  65. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/docs/reference/modules/private_message.md +0 -0
  66. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/docs/reference/modules/recommend.md +0 -0
  67. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/docs/reference/modules/search.md +0 -0
  68. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/docs/reference/modules/singer.md +0 -0
  69. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/docs/reference/modules/song.md +0 -0
  70. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/docs/reference/modules/songlist.md +0 -0
  71. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/docs/reference/modules/top.md +0 -0
  72. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/docs/reference/modules/user.md +0 -0
  73. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/docs/tutorial/client.md +0 -0
  74. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/docs/tutorial/credential.md +0 -0
  75. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/docs/tutorial/pagination.md +0 -0
  76. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/docs/tutorial/start.md +0 -0
  77. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/docs/tutorial/web.md +0 -0
  78. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/examples/download_song.py +0 -0
  79. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/examples/phone_login.py +0 -0
  80. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/examples/private_message.py +0 -0
  81. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/examples/qrcode_login.py +0 -0
  82. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/prek.toml +0 -0
  83. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/pyproject.toml +0 -0
  84. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/qqmusic_api/algorithms/__init__.py +0 -0
  85. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/qqmusic_api/algorithms/tripledes.py +0 -0
  86. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/qqmusic_api/core/__init__.py +0 -0
  87. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/qqmusic_api/core/client.py +0 -0
  88. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/qqmusic_api/core/exceptions.py +0 -0
  89. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/qqmusic_api/core/pagination.py +0 -0
  90. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/qqmusic_api/core/request.py +0 -0
  91. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/qqmusic_api/core/versioning.py +0 -0
  92. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/qqmusic_api/models/__init__.py +0 -0
  93. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/qqmusic_api/models/base.py +0 -0
  94. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/qqmusic_api/models/comment.py +0 -0
  95. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/qqmusic_api/models/login.py +0 -0
  96. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/qqmusic_api/models/lyric.py +0 -0
  97. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/qqmusic_api/models/mv.py +0 -0
  98. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/qqmusic_api/models/recommend.py +0 -0
  99. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/qqmusic_api/models/search.py +0 -0
  100. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/qqmusic_api/models/song.py +0 -0
  101. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/qqmusic_api/models/songlist.py +0 -0
  102. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/qqmusic_api/modules/__init__.py +0 -0
  103. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/qqmusic_api/modules/_base.py +0 -0
  104. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/qqmusic_api/modules/album.py +0 -0
  105. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/qqmusic_api/modules/comment.py +0 -0
  106. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/qqmusic_api/modules/login.py +0 -0
  107. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/qqmusic_api/modules/login_utils.py +0 -0
  108. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/qqmusic_api/modules/lyric.py +0 -0
  109. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/qqmusic_api/modules/mv.py +0 -0
  110. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/qqmusic_api/modules/private_message.py +0 -0
  111. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/qqmusic_api/modules/recommend.py +0 -0
  112. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/qqmusic_api/modules/search.py +0 -0
  113. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/qqmusic_api/modules/singer.py +0 -0
  114. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/qqmusic_api/modules/song.py +0 -0
  115. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/qqmusic_api/modules/songlist.py +0 -0
  116. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/qqmusic_api/modules/top.py +0 -0
  117. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/qqmusic_api/modules/user.py +0 -0
  118. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/qqmusic_api/utils/__init__.py +0 -0
  119. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/qqmusic_api/utils/common.py +0 -0
  120. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/qqmusic_api/utils/device.py +0 -0
  121. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/qqmusic_api/utils/mqtt.py +0 -0
  122. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/qqmusic_api/utils/qimei.py +0 -0
  123. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/scripts/ag-1.py +0 -0
  124. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/tests/test_comment.py +0 -0
  125. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/tests/test_login.py +0 -0
  126. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/tests/test_login_utils.py +0 -0
  127. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/tests/test_lyric.py +0 -0
  128. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/tests/test_mv.py +0 -0
  129. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/tests/test_private_message.py +0 -0
  130. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/tests/test_recommend.py +0 -0
  131. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/tests/test_search.py +0 -0
  132. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/tests/test_singer.py +0 -0
  133. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/tests/test_song.py +0 -0
  134. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/tests/test_songlist.py +0 -0
  135. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/tests/test_top.py +0 -0
  136. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/uv.lock +0 -0
  137. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/web/.gitignore +0 -0
  138. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/web/Dockerfile +0 -0
  139. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/web/README.md +0 -0
  140. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/web/accounts.example.toml +0 -0
  141. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/web/config.example.toml +0 -0
  142. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/web/docker-compose.yml +0 -0
  143. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/web/run.py +0 -0
  144. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/web/src/app.py +0 -0
  145. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/web/src/core/__init__.py +0 -0
  146. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/web/src/core/auth.py +0 -0
  147. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/web/src/core/cache.py +0 -0
  148. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/web/src/core/config.py +0 -0
  149. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/web/src/core/credential_store.py +0 -0
  150. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/web/src/core/deps.py +0 -0
  151. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/web/src/core/response.py +0 -0
  152. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/web/src/core/security.py +0 -0
  153. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/web/src/modules/__init__.py +0 -0
  154. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/web/src/modules/comment.py +0 -0
  155. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/web/src/modules/login.py +0 -0
  156. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/web/src/modules/mv.py +0 -0
  157. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/web/src/modules/singer.py +0 -0
  158. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/web/src/modules/song.py +0 -0
  159. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/web/src/modules/songlist.py +0 -0
  160. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/web/src/routes/__init__.py +0 -0
  161. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/web/src/routes/_helpers.py +0 -0
  162. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/web/src/routes/album.py +0 -0
  163. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/web/src/routes/comment.py +0 -0
  164. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/web/src/routes/login.py +0 -0
  165. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/web/src/routes/lyric.py +0 -0
  166. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/web/src/routes/mv.py +0 -0
  167. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/web/src/routes/recommend.py +0 -0
  168. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/web/src/routes/search.py +0 -0
  169. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/web/src/routes/singer.py +0 -0
  170. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/web/src/routes/song.py +0 -0
  171. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/web/src/routes/songlist.py +0 -0
  172. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/web/src/routes/top.py +0 -0
  173. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/web/src/routes/user.py +0 -0
  174. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/web/src/routing/__init__.py +0 -0
  175. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/web/src/routing/docstrings.py +0 -0
  176. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/web/src/routing/executor.py +0 -0
  177. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/web/src/routing/params.py +0 -0
  178. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/web/src/routing/route_types.py +0 -0
  179. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/web/src/routing/router_factory.py +0 -0
  180. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/web/tests/test_web_docstrings.py +0 -0
  181. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/web/tests/test_web_enums.py +0 -0
  182. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/web/tests/test_web_route_validation.py +0 -0
  183. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/web/tests/test_web_routes.py +0 -0
  184. {qqmusic_api_python-0.6.2 → qqmusic_api_python-0.6.3}/zensical.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qqmusic-api-python
3
- Version: 0.6.2
3
+ Version: 0.6.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,4 +1,29 @@
1
1
 
2
+ ## [[0.6.2](https://github.com/L-1124/QQMusicApi/compare/v0.6.1..v0.6.2)] - 2026-06-08
3
+
4
+ ### Bug 修复
5
+
6
+ * **(core)** 将 niquests 网络异常转换为`NetworkError`抛出" ([ff659c5](https://github.com/L-1124/QQMusicApi/commit/ff659c57dae8ad73db74cb00c5ba092b28f36466)) by [@L-1124](https://github.com/L-1124)
7
+ * **(core)** 修复 ANDROID 会话初始化的循环依赖和缓存失效问题 ([4f9be43](https://github.com/L-1124/QQMusicApi/commit/4f9be437724497a789265ede40368628858aea47)) by [@L-1124](https://github.com/L-1124)
8
+ * **(recommend)** 更新获取猜你喜欢推荐接口,支持传入 Credential, 添加 uid 字段到版本策略 ([81062d0](https://github.com/L-1124/QQMusicApi/commit/81062d0091883abb6904562a4ce5223c295d2b24)) by [@L-1124](https://github.com/L-1124)
9
+
10
+ ### 功能更新
11
+
12
+ * **(core)** 添加 getSession 匿名会话初始化和设备 OpenUDID 持久化 ([9a4532e](https://github.com/L-1124/QQMusicApi/commit/9a4532e922d3805ba60bc31b213efddbc4c18702)) by [@L-1124](https://github.com/L-1124)
13
+ * **(web)** 补充评论增删路由及推荐认证支持 ([08c9137](https://github.com/L-1124/QQMusicApi/commit/08c9137cb8adcc4ce29c0843f25b4a160819839b)) by [@L-1124](https://github.com/L-1124)
14
+
15
+ ### 功能重构
16
+
17
+ * **(web)** 重构日志系统 ([20fe8c6](https://github.com/L-1124/QQMusicApi/commit/20fe8c62af0477c5bf1e4afd82f08dcd96881031)) by [@L-1124](https://github.com/L-1124)
18
+ * **(web)** 统一代码风格与日志惰性求值 ([4ba0bf0](https://github.com/L-1124/QQMusicApi/commit/4ba0bf06b0e3a88bda6efea699c60ec068b16e91)) by [@L-1124](https://github.com/L-1124)
19
+ * **(web)** 补齐路由辅助函数与应用入口类型注解 ([080b2fe](https://github.com/L-1124/QQMusicApi/commit/080b2fe99969f7256005c5a2f23793cacfd6db85)) by [@L-1124](https://github.com/L-1124)
20
+ * **(web/routing)** 补齐路由类型与参数校验器类型注解 ([bcedc66](https://github.com/L-1124/QQMusicApi/commit/bcedc66bbb34d768957b36493250e067c4714e74)) by [@L-1124](https://github.com/L-1124)
21
+
22
+ ### 贡献者
23
+
24
+ * @L-1124
25
+ * @github-actions[bot]
26
+
2
27
  ## [[0.6.1](https://github.com/L-1124/QQMusicApi/compare/v0.6.0..v0.6.1)] - 2026-05-20
3
28
 
4
29
  ### Bug 修复
@@ -18,7 +18,7 @@ from .core.exceptions import (
18
18
  from .core.versioning import Platform
19
19
  from .models.request import Credential
20
20
 
21
- __version__ = "0.6.2"
21
+ __version__ = "0.6.3"
22
22
 
23
23
  __all__ = [
24
24
  "ApiDataError",
@@ -0,0 +1,30 @@
1
+ """Pydantic Validator."""
2
+
3
+ from typing import Any
4
+
5
+ from pydantic import BeforeValidator
6
+
7
+
8
+ def _none_to_empty_list(value: list[Any] | None) -> list[Any]:
9
+ """将 ``None`` 规整为空列表."""
10
+ return [] if value is None else value
11
+
12
+
13
+ def _none_to_empty_dict(value: dict[str, Any] | None) -> dict[str, Any]:
14
+ """将 ``None`` 规整为空字典."""
15
+ return {} if value is None else value
16
+
17
+
18
+ def _none_or_zero_to_empty_str(value: str | int | None) -> str:
19
+ """将 ``None`` 或 ``0`` 规整为空字符串."""
20
+ return "" if value in (None, 0) else str(value)
21
+
22
+
23
+ #: 将 ``None`` 规整为空列表的 BeforeValidator.
24
+ NoneToEmptyList = BeforeValidator(_none_to_empty_list)
25
+
26
+ #: 将 ``None`` 规整为空字典的 BeforeValidator.
27
+ NoneToEmptyDict = BeforeValidator(_none_to_empty_dict)
28
+
29
+ #: 将 ``None`` 或 ``0`` 规整为空字符串的 BeforeValidator.
30
+ NoneOrZeroToEmptyStr = BeforeValidator(_none_or_zero_to_empty_str)
@@ -1,6 +1,6 @@
1
1
  """Album API 返回模型定义."""
2
2
 
3
- from pydantic import Field, field_validator
3
+ from pydantic import Field
4
4
 
5
5
  from .base import Album, Singer, Song
6
6
  from .request import Response
@@ -70,9 +70,3 @@ 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
@@ -1,9 +1,10 @@
1
1
  """私信模块返回模型定义."""
2
2
 
3
- from typing import Any
3
+ from typing import Annotated, Any
4
4
 
5
- from pydantic import Field, field_validator, model_validator
5
+ from pydantic import Field, model_validator
6
6
 
7
+ from ._validator import NoneToEmptyDict
7
8
  from .request import Response
8
9
 
9
10
 
@@ -208,19 +209,13 @@ class PrivateMessageListResponse(Response):
208
209
  session: PrivateMessageSession | None = None
209
210
  subcode: int = 0
210
211
  end_msg_seq: int = 0
211
- attach: dict[str, Any] = Field(default_factory=dict, alias="Attach")
212
+ attach: Annotated[dict[str, Any], NoneToEmptyDict] = Field(default_factory=dict, alias="Attach")
212
213
  pat_interval: int = Field(default=0, alias="PatInterval")
213
- pat_map: dict[str, PrivateMessagePatText] = Field(default_factory=dict, alias="PatMap")
214
+ pat_map: Annotated[dict[str, PrivateMessagePatText], NoneToEmptyDict] = Field(default_factory=dict, alias="PatMap")
214
215
  encrypt_star: str = Field(default="", alias="EncryptStar")
215
216
  location_tips: str = Field(default="", alias="LocationTips")
216
217
  new_msg_cnt: int = Field(default=0, alias="NewMsgCnt")
217
218
 
218
- @field_validator("attach", "pat_map", mode="before")
219
- @classmethod
220
- def _normalize_nullable_fields(cls, value: Any) -> Any:
221
- """将服务端返回的空映射归一为空字典."""
222
- return {} if value is None else value
223
-
224
219
 
225
220
  class PrivateSendMessageResponse(Response):
226
221
  """私信发送响应.
@@ -179,9 +179,14 @@ class Response(BaseModel):
179
179
  matches = jsonpath_expr.find(data)
180
180
 
181
181
  if matches:
182
- if len(matches) == 1:
183
- processed_data[target_key] = matches[0].value
182
+ values = [match.value for match in matches]
183
+ if "[*]" in jsonpath_expr_str:
184
+ # 通配符路径表示提取条目列表, 即使仅命中一条也保持列表,
185
+ # 兼容上游在仅有一条数据时直接返回对象而非数组的行为.
186
+ processed_data[target_key] = values
187
+ elif len(values) == 1:
188
+ processed_data[target_key] = values[0]
184
189
  else:
185
- processed_data[target_key] = [match.value for match in matches]
190
+ processed_data[target_key] = values
186
191
 
187
192
  return processed_data
@@ -1,9 +1,10 @@
1
1
  """Singer API 返回模型定义."""
2
2
 
3
- from typing import Any
3
+ from typing import Annotated, Any
4
4
 
5
- from pydantic import AliasChoices, Field, field_validator
5
+ from pydantic import AliasChoices, Field
6
6
 
7
+ from ._validator import NoneOrZeroToEmptyStr, NoneToEmptyList
7
8
  from .base import MV, Album, Singer, Song
8
9
  from .request import Response
9
10
 
@@ -62,16 +63,10 @@ class SingerTagData(Response):
62
63
  index: 索引标签列表.
63
64
  """
64
65
 
65
- @field_validator("area", "genre", "sex", "index", mode="before")
66
- @classmethod
67
- def _coerce_none_list(cls, value: list[TagOption] | None) -> list[TagOption]:
68
- """将缺失的标签列表规整为空列表."""
69
- return [] if value is None else value
70
-
71
- area: list[TagOption] = Field(default_factory=list)
72
- genre: list[TagOption] = Field(default_factory=list)
73
- sex: list[TagOption] = Field(default_factory=list)
74
- index: list[TagOption] = Field(default_factory=list)
66
+ area: Annotated[list[TagOption], NoneToEmptyList] = Field(default_factory=list)
67
+ genre: Annotated[list[TagOption], NoneToEmptyList] = Field(default_factory=list)
68
+ sex: Annotated[list[TagOption], NoneToEmptyList] = Field(default_factory=list)
69
+ index: Annotated[list[TagOption], NoneToEmptyList] = Field(default_factory=list)
75
70
 
76
71
 
77
72
  class SingerTypeListResponse(Response):
@@ -179,12 +174,6 @@ class AlbumBrief(Album):
179
174
  tags: 标签列表.
180
175
  """
181
176
 
182
- @field_validator("tags", mode="before")
183
- @classmethod
184
- def _coerce_tags(cls, value: list[str] | None) -> list[str]:
185
- """将专辑标签中的空值规整为空列表."""
186
- return [] if value is None else value
187
-
188
177
  id: int = Field(default=-1, alias="albumID")
189
178
  mid: str = Field(default="", alias="albumMid")
190
179
  name: str = Field(default="", alias="albumName")
@@ -193,7 +182,7 @@ class AlbumBrief(Album):
193
182
  total_num: int = Field(default=0, alias="totalNum")
194
183
  album_type: str = Field(default="", alias="albumType")
195
184
  singer_name: str = Field(default="", alias="singerName")
196
- tags: list[str] = Field(default_factory=list)
185
+ tags: Annotated[list[str], NoneToEmptyList] = Field(default_factory=list)
197
186
 
198
187
 
199
188
  class VideoBrief(MV):
@@ -239,24 +228,24 @@ class HomepageTabDetailResponse(Response):
239
228
  video_tab: 视频标签内容.
240
229
  """
241
230
 
242
- @field_validator("tab_list", "introduction_tab", "song_tab", "album_tab", "video_tab", mode="before")
243
- @classmethod
244
- def _coerce_tab_lists(cls, value: list[Any] | None) -> list[Any]:
245
- """将标签详情中的空列表占位规整为列表."""
246
- return [] if value is None else value
247
-
248
231
  tab_id: str = Field(default="", alias="TabID")
249
232
  has_more: int = Field(default=0, alias="HasMore")
250
233
  need_show_tab: int = Field(default=0, alias="NeedShowTab")
251
234
  order: int = Field(default=0, alias="Order")
252
- tab_list: list[TabMeta] = Field(default_factory=list, alias="TabList")
253
- introduction_tab: list[dict[str, Any]] = Field(
235
+ tab_list: Annotated[list[TabMeta], NoneToEmptyList] = Field(default_factory=list, alias="TabList")
236
+ introduction_tab: Annotated[list[dict[str, Any]], NoneToEmptyList] = Field(
254
237
  default_factory=list,
255
238
  json_schema_extra={"jsonpath": "$.IntroductionTab.List"},
256
239
  )
257
- song_tab: list[Song] = Field(default_factory=list, json_schema_extra={"jsonpath": "$.SongTab.List[*]"})
258
- album_tab: list[AlbumBrief] = Field(default_factory=list, json_schema_extra={"jsonpath": "$.AlbumTab.AlbumList[*]"})
259
- video_tab: list[VideoBrief] = Field(default_factory=list, json_schema_extra={"jsonpath": "$.VideoTab.VideoList[*]"})
240
+ song_tab: Annotated[list[Song], NoneToEmptyList] = Field(
241
+ default_factory=list, json_schema_extra={"jsonpath": "$.SongTab.List[*]"}
242
+ )
243
+ album_tab: Annotated[list[AlbumBrief], NoneToEmptyList] = Field(
244
+ default_factory=list, json_schema_extra={"jsonpath": "$.AlbumTab.AlbumList[*]"}
245
+ )
246
+ video_tab: Annotated[list[VideoBrief], NoneToEmptyList] = Field(
247
+ default_factory=list, json_schema_extra={"jsonpath": "$.VideoTab.VideoList[*]"}
248
+ )
260
249
 
261
250
 
262
251
  class HomepageHeaderResponse(Response):
@@ -315,21 +304,15 @@ class SingerExtraInfo(Response):
315
304
  blog_flag: 博客标记.
316
305
  """
317
306
 
318
- @field_validator("area", "identity", "instrument", "genre", "enter", mode="before")
319
- @classmethod
320
- def _coerce_optional_text(cls, value: str | int | None) -> str:
321
- """将上游返回的 0 或空值规整为空字符串."""
322
- return "" if value in (None, 0) else str(value)
323
-
324
- area: str = ""
307
+ area: Annotated[str, NoneOrZeroToEmptyStr] = ""
325
308
  desc: str = ""
326
309
  tag: str = ""
327
- identity: str = ""
328
- instrument: str = ""
329
- genre: str = ""
310
+ identity: Annotated[str, NoneOrZeroToEmptyStr] = ""
311
+ instrument: Annotated[str, NoneOrZeroToEmptyStr] = ""
312
+ genre: Annotated[str, NoneOrZeroToEmptyStr] = ""
330
313
  foreign_name: str = ""
331
314
  birthday: str = ""
332
- enter: str = ""
315
+ enter: Annotated[str, NoneOrZeroToEmptyStr] = ""
333
316
  blog_flag: int = Field(default=0, alias="blogFlag")
334
317
 
335
318
 
@@ -345,18 +328,12 @@ class SingerDetail(Response):
345
328
  group_info: 组合附加信息.
346
329
  """
347
330
 
348
- @field_validator("group_list", "photos", "group_info", mode="before")
349
- @classmethod
350
- def _coerce_detail_lists(cls, value: list[dict[str, Any]] | None) -> list[dict[str, Any]]:
351
- """将歌手详情中的空列表占位规整为列表."""
352
- return [] if value is None else value
353
-
354
331
  basic_info: SingerBasicInfo = Field(alias="basic_info")
355
332
  ex_info: SingerExtraInfo = Field(default_factory=SingerExtraInfo, alias="ex_info")
356
333
  wiki: str = ""
357
- group_list: list[dict[str, Any]] = Field(default_factory=list)
358
- photos: list[dict[str, Any]] = Field(default_factory=list)
359
- group_info: list[dict[str, Any]] = Field(default_factory=list)
334
+ group_list: Annotated[list[dict[str, Any]], NoneToEmptyList] = Field(default_factory=list)
335
+ photos: Annotated[list[dict[str, Any]], NoneToEmptyList] = Field(default_factory=list)
336
+ group_info: Annotated[list[dict[str, Any]], NoneToEmptyList] = Field(default_factory=list)
360
337
 
361
338
 
362
339
  class SingerDetailResponse(Response):
@@ -402,13 +379,7 @@ class SimilarSingerResponse(Response):
402
379
  err_msg: 错误消息.
403
380
  """
404
381
 
405
- @field_validator("singerlist", mode="before")
406
- @classmethod
407
- def _coerce_similar_list(cls, value: list[SimilarSinger] | None) -> list[SimilarSinger]:
408
- """将相似歌手列表中的空值规整为空列表."""
409
- return [] if value is None else value
410
-
411
- singerlist: list[SimilarSinger] = Field(default_factory=list)
382
+ singerlist: Annotated[list[SimilarSinger], NoneToEmptyList] = Field(default_factory=list)
412
383
  code: int = 0
413
384
  err_msg: str = Field(default="", alias="errMsg")
414
385
 
@@ -422,15 +393,11 @@ class SingerSongListResponse(Response):
422
393
  song_list: 当前页歌曲列表.
423
394
  """
424
395
 
425
- @field_validator("song_list", mode="before")
426
- @classmethod
427
- def _coerce_song_list(cls, value: list[Song] | None) -> list[Song]:
428
- """将歌曲列表中的空值规整为空列表."""
429
- return [] if value is None else value
430
-
431
396
  singer_mid: str = Field(default="", alias="singerMid")
432
397
  total_num: int = Field(default=0, alias="totalNum")
433
- song_list: list[Song] = Field(default_factory=list, json_schema_extra={"jsonpath": "$.songList[*].songInfo"})
398
+ song_list: Annotated[list[Song], NoneToEmptyList] = Field(
399
+ default_factory=list, json_schema_extra={"jsonpath": "$.songList[*].songInfo"}
400
+ )
434
401
 
435
402
 
436
403
  class SingerAlbumListResponse(Response):
@@ -442,15 +409,9 @@ class SingerAlbumListResponse(Response):
442
409
  album_list: 当前页专辑列表.
443
410
  """
444
411
 
445
- @field_validator("album_list", mode="before")
446
- @classmethod
447
- def _coerce_album_list(cls, value: list[AlbumBrief] | None) -> list[AlbumBrief]:
448
- """将专辑列表中的空值规整为空列表."""
449
- return [] if value is None else value
450
-
451
412
  singer_mid: str = Field(default="", alias="singerMid")
452
413
  total: int = 0
453
- album_list: list[AlbumBrief] = Field(default_factory=list, alias="albumList")
414
+ album_list: Annotated[list[AlbumBrief], NoneToEmptyList] = Field(default_factory=list, alias="albumList")
454
415
 
455
416
 
456
417
  class SingerMvListResponse(Response):
@@ -461,11 +422,5 @@ class SingerMvListResponse(Response):
461
422
  mv_list: 当前页 MV 列表.
462
423
  """
463
424
 
464
- @field_validator("mv_list", mode="before")
465
- @classmethod
466
- def _coerce_mv_list(cls, value: list[VideoBrief] | None) -> list[VideoBrief]:
467
- """将 MV 列表中的空值规整为空列表."""
468
- return [] if value is None else value
469
-
470
425
  total: int = 0
471
- mv_list: list[VideoBrief] = Field(default_factory=list, alias="list")
426
+ mv_list: Annotated[list[VideoBrief], NoneToEmptyList] = Field(default_factory=list, alias="list")
@@ -1,7 +1,10 @@
1
1
  """Top API 返回模型定义."""
2
2
 
3
- from pydantic import Field, field_validator
3
+ from typing import Annotated
4
4
 
5
+ from pydantic import Field
6
+
7
+ from ._validator import NoneToEmptyList
5
8
  from .base import Song
6
9
  from .request import Response
7
10
 
@@ -105,14 +108,8 @@ class TopDetailResponse(Response):
105
108
  index_info_list: 榜单索引信息列表.
106
109
  """
107
110
 
108
- @field_validator("song_tags", "ext_info_list", "index_info_list", mode="before")
109
- @classmethod
110
- def _coerce_none_list(cls, value: list[dict] | None) -> list[dict]:
111
- """将上游返回的空列表占位统一规整为列表."""
112
- return [] if value is None else value
113
-
114
111
  info: TopSummary = Field(alias="data")
115
112
  songs: list[Song] = Field(alias="songInfoList")
116
- song_tags: list[dict] = Field(default_factory=list, alias="songTagInfoList")
117
- ext_info_list: list[dict] = Field(default_factory=list, alias="extInfoList")
118
- index_info_list: list[dict] = Field(default_factory=list, alias="indexInfoList")
113
+ song_tags: Annotated[list[dict], NoneToEmptyList] = Field(default_factory=list, alias="songTagInfoList")
114
+ ext_info_list: Annotated[list[dict], NoneToEmptyList] = Field(default_factory=list, alias="extInfoList")
115
+ index_info_list: Annotated[list[dict], NoneToEmptyList] = Field(default_factory=list, alias="indexInfoList")
@@ -1,8 +1,8 @@
1
1
  """User API 返回模型定义."""
2
2
 
3
- from typing import Any, cast
3
+ from typing import Any
4
4
 
5
- from pydantic import AliasChoices, Field, field_validator
5
+ from pydantic import AliasChoices, Field
6
6
 
7
7
  from .base import MV, Album, Singer, SongList
8
8
  from .request import Response
@@ -253,6 +253,64 @@ class UserHomepageResponse(Response):
253
253
  tab_detail: dict[str, Any] = Field(alias="TabDetail")
254
254
 
255
255
 
256
+ class VipIdentity(Response):
257
+ """VIP 信息响应中的会员身份明细块.
258
+
259
+ Attributes:
260
+ vip: 绿钻会员标志.
261
+ huge_vip: 豪华绿钻会员标志.
262
+ huge_vip_start: 豪华绿钻生效时间.
263
+ huge_vip_end: 豪华绿钻到期时间.
264
+ year_flag: 年费会员标志.
265
+ huge_year_flag: 豪华年费会员标志.
266
+ twelve: 十二平台会员标志.
267
+ twelve_start: 十二平台会员生效时间.
268
+ twelve_end: 十二平台会员到期时间.
269
+ child_vip: 儿童会员标志.
270
+ exp_vip: 体验会员标志.
271
+ group_vip_flag: 家庭组会员标志.
272
+ group_vip_start: 家庭组会员生效时间.
273
+ group_vip_end: 家庭组会员到期时间.
274
+ cp_lover_flag: 情侣会员标志.
275
+ cp_lover_start: 情侣会员生效时间.
276
+ cp_lover_end: 情侣会员到期时间.
277
+ ad_vip_flag: 广告会员标志.
278
+ eight: 八平台会员标志.
279
+ eight_start: 八平台会员生效时间.
280
+ eight_end: 八平台会员到期时间.
281
+ level: 会员等级.
282
+ next_level: 下一会员等级.
283
+ icon: 官方等级徽章图地址.
284
+ purchase_url: 会员购买页地址.
285
+ """
286
+
287
+ vip: int = 0
288
+ huge_vip: int = Field(default=0, alias="HugeVip")
289
+ huge_vip_start: str = Field(default="", alias="HugeVipStart")
290
+ huge_vip_end: str = Field(default="", alias="HugeVipEnd")
291
+ year_flag: int = Field(default=0, alias="yearflag")
292
+ huge_year_flag: int = Field(default=0, alias="HugeYearFlag")
293
+ twelve: int = 0
294
+ twelve_start: str = Field(default="", alias="twelveStart")
295
+ twelve_end: str = Field(default="", alias="twelveEnd")
296
+ child_vip: int = Field(default=0, alias="ChildVip")
297
+ exp_vip: int = Field(default=0, alias="ExpVip")
298
+ group_vip_flag: int = Field(default=0, alias="GroupVipFlag")
299
+ group_vip_start: str = Field(default="", alias="GroupVipStart")
300
+ group_vip_end: str = Field(default="", alias="GroupVipEnd")
301
+ cp_lover_flag: int = Field(default=0, alias="CPLoverFlag")
302
+ cp_lover_start: str = Field(default="", alias="CPLoverStart")
303
+ cp_lover_end: str = Field(default="", alias="CPLoverEnd")
304
+ ad_vip_flag: int = Field(default=0, alias="AdVipFlag")
305
+ eight: int = 0
306
+ eight_start: str = Field(default="", alias="eightStart")
307
+ eight_end: str = Field(default="", alias="eightEnd")
308
+ level: int = 0
309
+ next_level: int = Field(default=0, alias="nextlevel")
310
+ icon: str = ""
311
+ purchase_url: str = Field(default="", alias="purchaseUrl")
312
+
313
+
256
314
  class VipUserInfo(Response):
257
315
  """VIP 信息响应中的用户权益摘要块.
258
316
 
@@ -280,14 +338,30 @@ class UserVipInfoResponse(Response):
280
338
  max_dir_num: 最大歌单数量.
281
339
  max_song_num: 最大歌曲数量.
282
340
  song_limit_msg: 歌曲上限提示文案.
341
+ svip: 超级会员标志.
342
+ star: 星级会员标志.
343
+ star_start: 星级会员生效时间.
344
+ star_end: 星级会员到期时间.
345
+ ystar: 年费星级会员标志.
346
+ ystar_start: 年费星级会员生效时间.
347
+ ystar_end: 年费星级会员到期时间.
348
+ identity: 会员身份明细.
283
349
  userinfo: 用户权益摘要.
284
350
  """
285
351
 
286
- auto_down: int = Field(default=0, alias="autoDown")
352
+ auto_down: int = Field(default=0, validation_alias=AliasChoices("auto_down", "autoDown", "autodown"))
287
353
  can_renew: int = Field(default=0, alias="canRenew")
288
- max_dir_num: int = Field(default=0, alias="maxDirNum")
289
- max_song_num: int = Field(default=0, alias="maxSongNum")
290
- song_limit_msg: str = Field(default="", alias="songLimitMsg")
354
+ max_dir_num: int = Field(default=0, validation_alias=AliasChoices("max_dir_num", "maxDirNum", "maxdirnum"))
355
+ max_song_num: int = Field(default=0, validation_alias=AliasChoices("max_song_num", "maxSongNum", "maxsongnum"))
356
+ song_limit_msg: str = Field(default="", validation_alias=AliasChoices("song_limit_msg", "songLimitMsg"))
357
+ svip: int = 0
358
+ star: int = 0
359
+ star_start: str = Field(default="", alias="starstart")
360
+ star_end: str = Field(default="", alias="starend")
361
+ ystar: int = 0
362
+ ystar_start: str = Field(default="", alias="ystarstart")
363
+ ystar_end: str = Field(default="", alias="ystarend")
364
+ identity: VipIdentity = Field(default_factory=VipIdentity)
291
365
  userinfo: VipUserInfo = Field(default_factory=VipUserInfo)
292
366
 
293
367
 
@@ -326,19 +400,6 @@ class UserRelationListResponse(Response):
326
400
  lock_msg: 锁定提示文案.
327
401
  """
328
402
 
329
- @field_validator("users", mode="before")
330
- @classmethod
331
- def _coerce_users(
332
- cls,
333
- value: RelationUser | dict[str, Any] | list[RelationUser] | list[dict[str, Any]] | None,
334
- ) -> list[RelationUser | dict[str, Any]]:
335
- """将单条关系用户结果规整为列表."""
336
- if value is None:
337
- return []
338
- if isinstance(value, list):
339
- return cast("list[RelationUser | dict[str, Any]]", value)
340
- return [value]
341
-
342
403
  total: int = Field(default=0, alias="Total")
343
404
  users: list[RelationUser] = Field(default_factory=list, json_schema_extra={"jsonpath": "$.List[*]"})
344
405
  has_more: bool = Field(default=False, alias="HasMore")
@@ -413,6 +474,6 @@ class UserFavMvResponse(Response):
413
474
  """
414
475
 
415
476
  code: int
416
- sub_code: int = Field(alias="subCode")
477
+ sub_code: int = Field(validation_alias=AliasChoices("subCode", "subcode"))
417
478
  msg: str
418
479
  mv_list: list[UserFavMvItem] = Field(alias="mvlist")
@@ -62,6 +62,20 @@ async def _retry_rate_limited_call(operation: Callable[[], Awaitable[Any]]) -> A
62
62
  raise RuntimeError("限流重试流程异常结束")
63
63
 
64
64
 
65
+ async def _call_with_skip(coro_fn: Callable[[], Awaitable[Any]]) -> Any:
66
+ """执行 API 调用, 将环境不可用异常转为 pytest.skip."""
67
+ try:
68
+ return await _retry_rate_limited_call(coro_fn)
69
+ except (CredentialInvalidError, CredentialExpiredError) as exc:
70
+ pytest.skip(str(exc))
71
+ except RatelimitedError as exc:
72
+ pytest.skip(f"{exc}。指数退避重试 {len(RATE_LIMIT_RETRY_DELAYS)} 次后仍触发限流")
73
+ except Exception as exc:
74
+ if _is_network_timeout_error(exc):
75
+ pytest.skip(str(exc))
76
+ raise
77
+
78
+
65
79
  @pytest.fixture(autouse=True)
66
80
  def handle_unavailable_api_errors(monkeypatch: pytest.MonkeyPatch):
67
81
  """为测试 API 调用添加限流重试, 并将环境不可用异常转为跳过."""
@@ -69,7 +83,7 @@ def handle_unavailable_api_errors(monkeypatch: pytest.MonkeyPatch):
69
83
  original_gather = Client.gather
70
84
 
71
85
  async def execute_with_rate_limit_retry(client: Client, request: Any) -> Any:
72
- return await _retry_rate_limited_call(lambda: original_execute(client, request))
86
+ return await _call_with_skip(lambda: original_execute(client, request))
73
87
 
74
88
  async def gather_with_rate_limit_retry(
75
89
  client: Client,
@@ -78,7 +92,7 @@ def handle_unavailable_api_errors(monkeypatch: pytest.MonkeyPatch):
78
92
  batch_size: int = 20,
79
93
  return_exceptions: bool = False,
80
94
  ) -> list[Any]:
81
- return await _retry_rate_limited_call(
95
+ return await _call_with_skip(
82
96
  lambda: original_gather(
83
97
  client,
84
98
  requests,
@@ -90,16 +104,7 @@ def handle_unavailable_api_errors(monkeypatch: pytest.MonkeyPatch):
90
104
  monkeypatch.setattr(Client, "execute", execute_with_rate_limit_retry)
91
105
  monkeypatch.setattr(Client, "gather", gather_with_rate_limit_retry)
92
106
 
93
- try:
94
- yield
95
- except (CredentialInvalidError, CredentialExpiredError) as exc:
96
- pytest.skip(str(exc))
97
- except RatelimitedError as exc:
98
- pytest.skip(f"{exc}。指数退避重试 {len(RATE_LIMIT_RETRY_DELAYS)} 次后仍触发限流")
99
- except Exception as exc:
100
- if _is_network_timeout_error(exc):
101
- pytest.skip(str(exc))
102
- raise
107
+ return
103
108
 
104
109
 
105
110
  @pytest_asyncio.fixture
@@ -16,14 +16,14 @@ async def test_get_detail_by_id(client: Client) -> None:
16
16
 
17
17
 
18
18
  @pytest.mark.parametrize(
19
- ("num", "page"),
19
+ ("mid", "num", "page"),
20
20
  [
21
- (30, 1),
22
- (5, 1),
23
- (10, 2),
21
+ ("001uKKpF1RuJSd", 30, 1),
22
+ ("001gR6jO1L4MWq", 5, 1),
23
+ ("0041WVfh2vtlJE", 10, 2),
24
24
  ],
25
25
  )
26
- async def test_get_song(client: Client, num: int, page: int) -> None:
26
+ async def test_get_song(client: Client, mid: str, num: int, page: int) -> None:
27
27
  """测试获取专辑歌曲列表."""
28
- result = await client.album.get_song(100, num=num, page=page)
28
+ result = await client.album.get_song(mid, num=num, page=page)
29
29
  assert result is not None
@@ -1,6 +1,9 @@
1
1
  """用户模块测试."""
2
2
 
3
+ import pytest
4
+
3
5
  from qqmusic_api import Client
6
+ from qqmusic_api.models.user import UserFavMvResponse
4
7
 
5
8
 
6
9
  async def test_get_homepage(client: Client) -> None:
@@ -36,9 +39,18 @@ async def test_relation_response_models_with_login(authenticated_client: Client)
36
39
  async def test_get_vip_info_with_login(authenticated_client: Client) -> None:
37
40
  """测试获取 VIP 信息模型."""
38
41
  result = await authenticated_client.user.get_vip_info()
39
- assert result.max_dir_num >= 0
40
- assert result.max_song_num >= 0
42
+ assert result.max_dir_num > 0
43
+ assert result.max_song_num > 0
41
44
  assert result.userinfo.music_level >= 0
45
+ assert result.svip >= 0
46
+ assert result.star >= 0
47
+ assert result.ystar >= 0
48
+ assert result.identity.vip >= 0
49
+ assert result.identity.huge_vip >= 0
50
+ assert result.identity.year_flag >= 0
51
+ assert result.identity.huge_year_flag >= 0
52
+ assert result.identity.eight >= 0
53
+ assert result.identity.level >= 0
42
54
 
43
55
 
44
56
  async def test_get_created_songlist_with_login(authenticated_client: Client) -> None:
@@ -101,3 +113,10 @@ async def test_get_fav_mv_with_login(authenticated_client: Client) -> None:
101
113
  assert result.code == 0
102
114
  assert result.sub_code == 0
103
115
  assert result.mv_list is not None
116
+
117
+
118
+ @pytest.mark.parametrize("sub_code_key", ["subCode", "subcode"])
119
+ def test_fav_mv_response_accepts_subcode_spellings(sub_code_key: str) -> None:
120
+ """测试收藏 MV 响应兼容子返回码键名的两种大小写拼写."""
121
+ result = UserFavMvResponse.model_validate({"code": 0, sub_code_key: 0, "msg": "", "mvlist": []})
122
+ assert result.sub_code == 0