qqmusic-api-python 0.6.1__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.1 → qqmusic_api_python-0.6.3}/PKG-INFO +1 -1
  2. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/release-notes.md +41 -0
  3. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/tutorial/client.md +12 -0
  4. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/__init__.py +1 -1
  5. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/core/client.py +135 -64
  6. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/core/request.py +1 -0
  7. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/core/versioning.py +4 -0
  8. qqmusic_api_python-0.6.3/qqmusic_api/models/_validator.py +30 -0
  9. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/models/album.py +1 -7
  10. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/models/private_message.py +5 -10
  11. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/models/recommend.py +2 -10
  12. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/models/request.py +11 -3
  13. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/models/singer.py +33 -78
  14. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/models/top.py +7 -10
  15. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/models/user.py +81 -20
  16. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/modules/login.py +3 -1
  17. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/modules/recommend.py +8 -2
  18. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/utils/device.py +4 -0
  19. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/utils/qimei.py +33 -36
  20. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/tests/conftest.py +17 -12
  21. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/tests/test_album.py +6 -6
  22. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/tests/test_user.py +21 -2
  23. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/uv.lock +256 -256
  24. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/run.py +23 -10
  25. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/src/app.py +19 -10
  26. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/src/core/auth.py +30 -30
  27. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/src/core/cache.py +14 -14
  28. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/src/core/config.py +7 -4
  29. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/src/core/credential_store.py +24 -21
  30. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/src/core/security.py +2 -0
  31. qqmusic_api_python-0.6.3/web/src/modules/comment.py +24 -0
  32. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/src/modules/login.py +28 -5
  33. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/src/modules/mv.py +1 -1
  34. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/src/modules/singer.py +1 -1
  35. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/src/modules/song.py +15 -4
  36. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/src/modules/songlist.py +1 -1
  37. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/src/routes/_helpers.py +9 -4
  38. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/src/routes/comment.py +33 -3
  39. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/src/routes/recommend.py +8 -2
  40. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/src/routing/params.py +27 -3
  41. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/src/routing/route_types.py +1 -1
  42. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/src/routing/router_factory.py +2 -1
  43. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/.agents/skills/pydantic/SKILL.md +0 -0
  44. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/.agents/skills/python-standards/SKILL.md +0 -0
  45. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/.agents/skills/tarsio/SKILL.md +0 -0
  46. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/.agents/skills/tarsio/references/api-reference.md +0 -0
  47. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/.agents/skills/uv-package-manager/SKILL.md +0 -0
  48. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/.dockerignore +0 -0
  49. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/.github/ISSUE_TEMPLATE/bug.yml +0 -0
  50. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  51. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/.github/ISSUE_TEMPLATE/feature.yml +0 -0
  52. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  53. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/.github/renovate.json +0 -0
  54. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/.github/workflows/checking.yaml +0 -0
  55. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/.github/workflows/docs.yml +0 -0
  56. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/.github/workflows/release.yml +0 -0
  57. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/.github/workflows/testing.yml +0 -0
  58. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/.gitignore +0 -0
  59. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/.markdownlint-cli2.yaml +0 -0
  60. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/AGENTS.md +0 -0
  61. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/CLAUDE.md +0 -0
  62. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/LICENSE +0 -0
  63. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/README.md +0 -0
  64. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/assets/qq-music.svg +0 -0
  65. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/cliff.toml +0 -0
  66. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/coding.md +0 -0
  67. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/contributing.md +0 -0
  68. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/index.md +0 -0
  69. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/reference/core/client.md +0 -0
  70. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/reference/core/exception.md +0 -0
  71. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/reference/core/pagination.md +0 -0
  72. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/reference/core/request.md +0 -0
  73. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/reference/core/versioning.md +0 -0
  74. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/reference/model/album.md +0 -0
  75. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/reference/model/base.md +0 -0
  76. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/reference/model/comment.md +0 -0
  77. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/reference/model/login.md +0 -0
  78. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/reference/model/lyric.md +0 -0
  79. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/reference/model/mv.md +0 -0
  80. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/reference/model/recommend.md +0 -0
  81. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/reference/model/request.md +0 -0
  82. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/reference/model/search.md +0 -0
  83. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/reference/model/singer.md +0 -0
  84. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/reference/model/song.md +0 -0
  85. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/reference/model/songlist.md +0 -0
  86. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/reference/model/top.md +0 -0
  87. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/reference/model/user.md +0 -0
  88. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/reference/modules/album.md +0 -0
  89. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/reference/modules/comment.md +0 -0
  90. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/reference/modules/login.md +0 -0
  91. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/reference/modules/login_utils.md +0 -0
  92. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/reference/modules/lyric.md +0 -0
  93. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/reference/modules/mv.md +0 -0
  94. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/reference/modules/private_message.md +0 -0
  95. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/reference/modules/recommend.md +0 -0
  96. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/reference/modules/search.md +0 -0
  97. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/reference/modules/singer.md +0 -0
  98. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/reference/modules/song.md +0 -0
  99. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/reference/modules/songlist.md +0 -0
  100. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/reference/modules/top.md +0 -0
  101. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/reference/modules/user.md +0 -0
  102. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/tutorial/credential.md +0 -0
  103. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/tutorial/pagination.md +0 -0
  104. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/tutorial/start.md +0 -0
  105. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/tutorial/web.md +0 -0
  106. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/examples/download_song.py +0 -0
  107. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/examples/phone_login.py +0 -0
  108. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/examples/private_message.py +0 -0
  109. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/examples/qrcode_login.py +0 -0
  110. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/prek.toml +0 -0
  111. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/pyproject.toml +0 -0
  112. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/algorithms/__init__.py +0 -0
  113. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/algorithms/tripledes.py +0 -0
  114. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/core/__init__.py +0 -0
  115. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/core/exceptions.py +0 -0
  116. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/core/pagination.py +0 -0
  117. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/models/__init__.py +0 -0
  118. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/models/base.py +0 -0
  119. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/models/comment.py +0 -0
  120. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/models/login.py +0 -0
  121. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/models/lyric.py +0 -0
  122. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/models/mv.py +0 -0
  123. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/models/search.py +0 -0
  124. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/models/song.py +0 -0
  125. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/models/songlist.py +0 -0
  126. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/modules/__init__.py +0 -0
  127. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/modules/_base.py +0 -0
  128. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/modules/album.py +0 -0
  129. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/modules/comment.py +0 -0
  130. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/modules/login_utils.py +0 -0
  131. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/modules/lyric.py +0 -0
  132. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/modules/mv.py +0 -0
  133. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/modules/private_message.py +0 -0
  134. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/modules/search.py +0 -0
  135. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/modules/singer.py +0 -0
  136. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/modules/song.py +0 -0
  137. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/modules/songlist.py +0 -0
  138. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/modules/top.py +0 -0
  139. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/modules/user.py +0 -0
  140. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/utils/__init__.py +0 -0
  141. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/utils/common.py +0 -0
  142. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/utils/mqtt.py +0 -0
  143. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/scripts/ag-1.py +0 -0
  144. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/tests/test_comment.py +0 -0
  145. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/tests/test_login.py +0 -0
  146. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/tests/test_login_utils.py +0 -0
  147. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/tests/test_lyric.py +0 -0
  148. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/tests/test_mv.py +0 -0
  149. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/tests/test_private_message.py +0 -0
  150. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/tests/test_recommend.py +0 -0
  151. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/tests/test_search.py +0 -0
  152. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/tests/test_singer.py +0 -0
  153. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/tests/test_song.py +0 -0
  154. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/tests/test_songlist.py +0 -0
  155. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/tests/test_top.py +0 -0
  156. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/.gitignore +0 -0
  157. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/Dockerfile +0 -0
  158. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/README.md +0 -0
  159. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/accounts.example.toml +0 -0
  160. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/config.example.toml +0 -0
  161. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/docker-compose.yml +0 -0
  162. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/src/core/__init__.py +0 -0
  163. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/src/core/deps.py +0 -0
  164. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/src/core/response.py +0 -0
  165. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/src/modules/__init__.py +0 -0
  166. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/src/routes/__init__.py +0 -0
  167. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/src/routes/album.py +0 -0
  168. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/src/routes/login.py +0 -0
  169. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/src/routes/lyric.py +0 -0
  170. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/src/routes/mv.py +0 -0
  171. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/src/routes/search.py +0 -0
  172. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/src/routes/singer.py +0 -0
  173. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/src/routes/song.py +0 -0
  174. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/src/routes/songlist.py +0 -0
  175. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/src/routes/top.py +0 -0
  176. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/src/routes/user.py +0 -0
  177. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/src/routing/__init__.py +0 -0
  178. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/src/routing/docstrings.py +0 -0
  179. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/src/routing/executor.py +0 -0
  180. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/tests/test_web_docstrings.py +0 -0
  181. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/tests/test_web_enums.py +0 -0
  182. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/tests/test_web_route_validation.py +0 -0
  183. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/tests/test_web_routes.py +0 -0
  184. {qqmusic_api_python-0.6.1 → 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.1
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,45 @@
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
+
27
+ ## [[0.6.1](https://github.com/L-1124/QQMusicApi/compare/v0.6.0..v0.6.1)] - 2026-05-20
28
+
29
+ ### Bug 修复
30
+
31
+ * 传入的platform不生效 ([b19bec5](https://github.com/L-1124/QQMusicApi/commit/b19bec52f044d6508d9506f7111fea6eee5b42de)) by [@L-1124](https://github.com/L-1124)
32
+
33
+ ### 功能更新
34
+
35
+ * **(comment)** 支持添加评论和删除评论功能 ([dc5f568](https://github.com/L-1124/QQMusicApi/commit/dc5f5685d31f48b5dd8fc6bfcfdb6fb357884e7e)) by [@L-1124](https://github.com/L-1124)
36
+ * **(private-message)** 新增私信接口模块 ([0ce6c77](https://github.com/L-1124/QQMusicApi/commit/0ce6c77b423c01c684cf86ff7a7ee9ca9a70d9fa)) by [@L-1124](https://github.com/L-1124)
37
+
38
+ ### 贡献者
39
+
40
+ * @L-1124
41
+ * @github-actions[bot]
42
+
2
43
  ## [[0.6.0](https://github.com/L-1124/QQMusicApi/compare/v0.5.3..v0.6.0)] - 2026-05-09
3
44
 
4
45
  ### Bug 修复
@@ -94,3 +94,15 @@ async def main():
94
94
  asyncio.run(_main())
95
95
 
96
96
  ```
97
+
98
+ ## 设备信息
99
+
100
+ 可通过 `device_path` 参数指定设备信息文件的路径进行持久化存储:
101
+
102
+ ```python
103
+ client = Client(device_path="device.json")
104
+ ```
105
+
106
+ 不传 `device_path` 则仅在内存维护设备状态,重启后丢失。
107
+
108
+ `Client.credential` 更改时设备信息保持不变。
@@ -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.1"
21
+ __version__ = "0.6.3"
22
22
 
23
23
  __all__ = [
24
24
  "ApiDataError",
@@ -1,11 +1,12 @@
1
1
  """API 客户端核心实现. 整合网络传输、鉴权与业务模块访问."""
2
2
 
3
- import uuid
4
3
  from collections import defaultdict
5
4
  from functools import cached_property
6
5
  from typing import TYPE_CHECKING, Any, Literal, cast, overload
7
6
 
7
+ import anyio
8
8
  from niquests import AsyncSession, AsyncTokenBucketLimiter, PreparedRequest
9
+ from niquests.exceptions import RequestException
9
10
  from niquests.models import Response
10
11
  from niquests.typing import AsyncHookType, ProxyType, TLSClientCertType, TLSVerifyType
11
12
  from tarsio import TarsDict
@@ -21,6 +22,7 @@ from .exceptions import (
21
22
  CredentialExpiredError,
22
23
  GlobalApiError,
23
24
  HTTPError,
25
+ NetworkError,
24
26
  RatelimitedError,
25
27
  )
26
28
  from .request import Request, RequestResultT, _build_result
@@ -99,8 +101,9 @@ class Client:
99
101
 
100
102
  self._device_store = DeviceManager(device_path)
101
103
 
102
- self._guid = uuid.uuid4().hex
103
104
  self._version_policy: VersionPolicy = DEFAULT_VERSION_POLICY
105
+ self._session_lock = anyio.Lock()
106
+ self._session_initialized = False
104
107
  self._qimei_manager = QimeiManager(
105
108
  device_store=self._device_store,
106
109
  app_version=self._version_policy.get_qimei_app_version(),
@@ -108,6 +111,62 @@ class Client:
108
111
  session=self._session,
109
112
  )
110
113
 
114
+ async def _ensure_session(self) -> None:
115
+ async with self._session_lock:
116
+ if self._session_initialized:
117
+ return
118
+ device = await self._device_store.get_device()
119
+ if device.session_uid and device.session_sid:
120
+ self._session_initialized = True
121
+ return
122
+
123
+ finalcomm = self._version_policy.build_comm(
124
+ platform=Platform.ANDROID,
125
+ credential=self.credential,
126
+ device=device,
127
+ qimei=cast("dict[str, str]", await self._qimei_manager.get_cached()),
128
+ guid=device.open_udid,
129
+ )
130
+ payload: dict[str, Any] = {
131
+ "comm": finalcomm,
132
+ "req_0": {
133
+ "module": "music.getSession.session",
134
+ "method": "GetSession",
135
+ "param": {
136
+ "uid": device.session_uid or "",
137
+ "vkey": 0,
138
+ "caller": 0,
139
+ },
140
+ },
141
+ }
142
+ user_agent = await self._get_user_agent(Platform.ANDROID)
143
+ try:
144
+ resp = await self._session.post(
145
+ "https://u.y.qq.com/cgi-bin/musicu.fcg",
146
+ json=payload,
147
+ headers={"User-Agent": user_agent},
148
+ proxies=self.proxies,
149
+ hooks=self.hooks,
150
+ cert=self.cert,
151
+ verify=self.verify,
152
+ )
153
+ await self._session.gather(resp)
154
+ except RequestException as exc:
155
+ raise NetworkError(str(exc)) from exc
156
+ if resp.status_code != 200:
157
+ raise HTTPError(
158
+ f"HTTP 请求状态码异常: {resp.status_code}",
159
+ status_code=cast("int", resp.status_code),
160
+ )
161
+
162
+ resp_data = resp.json()
163
+ session_data = resp_data["req_0"]["data"]["session"]
164
+ device.session_uid = str(session_data["uid"])
165
+ device.session_sid = session_data["sid"]
166
+ device.session_vkey = session_data.get("vkey")
167
+ await self._device_store.save_device()
168
+ self._session_initialized = True
169
+
111
170
  @cached_property
112
171
  def comment(self) -> "CommentApi":
113
172
  """评论模块."""
@@ -261,18 +320,21 @@ class Client:
261
320
  headers["User-Agent"] = await self._get_user_agent(platform)
262
321
  kwargs["headers"] = headers
263
322
 
264
- resp = await self._session.request(
265
- method,
266
- url,
267
- **kwargs,
268
- proxies=self.proxies,
269
- hooks=self.hooks,
270
- cert=self.cert,
271
- verify=self.verify,
272
- )
273
- if not lazy:
274
- await self._session.gather(resp)
275
- return resp
323
+ try:
324
+ resp = await self._session.request(
325
+ method,
326
+ url,
327
+ **kwargs,
328
+ proxies=self.proxies,
329
+ hooks=self.hooks,
330
+ cert=self.cert,
331
+ verify=self.verify,
332
+ )
333
+ if not lazy:
334
+ await self._session.gather(resp)
335
+ return resp
336
+ except RequestException as exc:
337
+ raise NetworkError(str(exc)) from exc
276
338
 
277
339
  async def request_api(
278
340
  self,
@@ -285,39 +347,68 @@ class Client:
285
347
  lazy: bool = False,
286
348
  ) -> Response:
287
349
  """发送 API 请求."""
288
- platform = Platform.ANDROID if is_jce else platform or self.platform
350
+ target_platform = Platform.ANDROID if is_jce else platform or self.platform
351
+ if target_platform == Platform.ANDROID:
352
+ await self._ensure_session()
353
+ device = await self._device_store.get_device()
289
354
  finalcomm = self._version_policy.build_comm(
290
- platform=platform or self.platform,
355
+ platform=target_platform,
291
356
  credential=credential or self.credential,
292
- device=await self._device_store.get_device(),
357
+ device=device,
293
358
  qimei=cast("dict[str, str]", await self._qimei_manager.get_cached())
294
- if platform == Platform.ANDROID
359
+ if target_platform == Platform.ANDROID
295
360
  else None,
296
- guid=self._guid,
361
+ guid=device.open_udid,
297
362
  )
298
363
  if comm:
299
364
  finalcomm.update(comm)
300
365
 
301
- user_agent = await self._get_user_agent(platform)
366
+ user_agent = await self._get_user_agent(target_platform)
367
+
368
+ try:
369
+ if is_jce:
370
+ for k, v in finalcomm.items():
371
+ if not isinstance(v, str):
372
+ finalcomm[k] = str(v)
373
+ content = JceRequest(
374
+ finalcomm,
375
+ {
376
+ f"req_{idx}": JceRequestItem(
377
+ module=req["module"],
378
+ method=req["method"],
379
+ param=TarsDict(cast("dict[int, Any]", req["param"])),
380
+ )
381
+ for idx, req in enumerate(data)
382
+ },
383
+ ).encode()
384
+ resp = await self._session.post(
385
+ "http://u.y.qq.com/cgi-bin/musicw.fcg",
386
+ data=content,
387
+ headers={"User-Agent": user_agent},
388
+ proxies=self.proxies,
389
+ hooks=self.hooks,
390
+ cert=self.cert,
391
+ verify=self.verify,
392
+ )
393
+ if not lazy:
394
+ await self._session.gather(resp)
395
+ return resp
396
+
397
+ payload: dict[str, Any] = {
398
+ "comm": finalcomm,
399
+ }
400
+ params = {}
401
+ for idx, req in enumerate(data):
402
+ payload[f"req_{idx}"] = {
403
+ "module": req["module"],
404
+ "method": req["method"],
405
+ "param": req["param"] if req["preserve_bool"] else bool_to_int(req["param"]),
406
+ }
302
407
 
303
- if is_jce:
304
- for k, v in finalcomm.items():
305
- if not isinstance(v, str):
306
- finalcomm[k] = str(v)
307
- content = JceRequest(
308
- finalcomm,
309
- {
310
- f"req_{idx}": JceRequestItem(
311
- module=req["module"],
312
- method=req["method"],
313
- param=TarsDict(cast("dict[int, Any]", req["param"])),
314
- )
315
- for idx, req in enumerate(data)
316
- },
317
- ).encode()
318
408
  resp = await self._session.post(
319
- "http://u.y.qq.com/cgi-bin/musicw.fcg",
320
- data=content,
409
+ "https://u.y.qq.com/cgi-bin/musicu.fcg",
410
+ json=payload,
411
+ params=params,
321
412
  headers={"User-Agent": user_agent},
322
413
  proxies=self.proxies,
323
414
  hooks=self.hooks,
@@ -326,33 +417,10 @@ class Client:
326
417
  )
327
418
  if not lazy:
328
419
  await self._session.gather(resp)
329
- return resp
330
-
331
- payload: dict[str, Any] = {
332
- "comm": finalcomm,
333
- }
334
- params = {}
335
- for idx, req in enumerate(data):
336
- payload[f"req_{idx}"] = {
337
- "module": req["module"],
338
- "method": req["method"],
339
- "param": req["param"] if req["preserve_bool"] else bool_to_int(req["param"]),
340
- }
341
420
 
342
- resp = await self._session.post(
343
- "https://u.y.qq.com/cgi-bin/musicu.fcg",
344
- json=payload,
345
- params=params,
346
- headers={"User-Agent": user_agent},
347
- proxies=self.proxies,
348
- hooks=self.hooks,
349
- cert=self.cert,
350
- verify=self.verify,
351
- )
352
- if not lazy:
353
- await self._session.gather(resp)
354
-
355
- return resp
421
+ return resp
422
+ except RequestException as exc:
423
+ raise NetworkError(str(exc)) from exc
356
424
 
357
425
  @overload
358
426
  async def gather(
@@ -451,7 +519,10 @@ class Client:
451
519
  )
452
520
  batch_responses.append((batch_indices, response_task))
453
521
 
454
- await self._session.gather(*(resp for _, resp in batch_responses))
522
+ try:
523
+ await self._session.gather(*(resp for _, resp in batch_responses))
524
+ except RequestException as exc:
525
+ raise NetworkError(str(exc)) from exc
455
526
 
456
527
  results: list[Any] = [_SENTINEL] * len(requests)
457
528
 
@@ -63,6 +63,7 @@ def _build_result(
63
63
  return response_model.model_validate(raw)
64
64
  return raw
65
65
 
66
+
66
67
  @dataclass(kw_only=True)
67
68
  class Request(Generic[RequestResultT]):
68
69
  """请求描述符."""
@@ -88,6 +88,8 @@ class VersionPolicy:
88
88
  device.model,
89
89
  device.version.sdk,
90
90
  device.fingerprint,
91
+ device.session_uid,
92
+ device.session_sid,
91
93
  )
92
94
  if platform == Platform.ANDROID
93
95
  else (),
@@ -113,7 +115,9 @@ class VersionPolicy:
113
115
  QIMEI36=qimei["q36"] if qimei is not None else "",
114
116
  OpenUDID=guid,
115
117
  udid=guid,
118
+ uid=device.session_uid,
116
119
  OpenUDID2=guid,
120
+ sid=device.session_sid,
117
121
  aid=device.android_id,
118
122
  os_ver=device.version.release,
119
123
  phonetype=device.model,
@@ -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
  """私信发送响应.
@@ -2,7 +2,7 @@
2
2
 
3
3
  from typing import Any
4
4
 
5
- from pydantic import AliasChoices, Field, model_validator
5
+ from pydantic import AliasChoices, Field
6
6
 
7
7
  from .base import Song, SongList
8
8
  from .request import Response
@@ -69,15 +69,7 @@ class GuessRecommendResponse(Response):
69
69
  songs: 推荐歌曲列表.
70
70
  """
71
71
 
72
- songs: list[Song] = Field(default_factory=list, alias="Tracks")
73
-
74
- @model_validator(mode="before")
75
- @classmethod
76
- def _normalize_tracks(cls, data: Any) -> Any:
77
- """将猜你喜欢响应规整为稳定的歌曲列表载荷."""
78
- if isinstance(data, dict) and "Tracks" not in data:
79
- return {"Tracks": []}
80
- return data
72
+ songs: list[Song] = Field(default_factory=list, alias="tracks")
81
73
 
82
74
 
83
75
  class RadarRecommendResponse(Response):
@@ -45,6 +45,8 @@ class CommonParams(BaseModel):
45
45
  udid: str | None = Field(default=None)
46
46
  aid: str | None = Field(default=None)
47
47
  guid: str | None = Field(default=None)
48
+ uid: str | None = Field(default=None)
49
+ sid: str | None = Field(default=None)
48
50
  os_ver: str | None = Field(default=None)
49
51
  phonetype: str | None = Field(default=None)
50
52
  devicelevel: str | None = Field(default=None)
@@ -102,6 +104,7 @@ class Credential(BaseModel):
102
104
  if "loginType" in data or "login_type" in data:
103
105
  return data
104
106
 
107
+ # TODO: 修复没有 musickey 时误判为 QQ 登录的问题
105
108
  musickey = data.get("musickey", "")
106
109
  inferred_login_type = 1 if isinstance(musickey, str) and musickey.startswith("W_X") else 2
107
110
  return {**data, "loginType": inferred_login_type}
@@ -176,9 +179,14 @@ class Response(BaseModel):
176
179
  matches = jsonpath_expr.find(data)
177
180
 
178
181
  if matches:
179
- if len(matches) == 1:
180
- 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]
181
189
  else:
182
- processed_data[target_key] = [match.value for match in matches]
190
+ processed_data[target_key] = values
183
191
 
184
192
  return processed_data