qqmusic-api-python 0.6.1__tar.gz → 0.6.2__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 (183) hide show
  1. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/PKG-INFO +1 -1
  2. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/docs/release-notes.md +16 -0
  3. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/docs/tutorial/client.md +12 -0
  4. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/qqmusic_api/__init__.py +1 -1
  5. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/qqmusic_api/core/client.py +135 -64
  6. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/qqmusic_api/core/request.py +1 -0
  7. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/qqmusic_api/core/versioning.py +4 -0
  8. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/qqmusic_api/models/recommend.py +2 -10
  9. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/qqmusic_api/models/request.py +3 -0
  10. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/qqmusic_api/modules/login.py +3 -1
  11. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/qqmusic_api/modules/recommend.py +8 -2
  12. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/qqmusic_api/utils/device.py +4 -0
  13. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/qqmusic_api/utils/qimei.py +33 -36
  14. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/uv.lock +256 -256
  15. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/web/run.py +23 -10
  16. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/web/src/app.py +19 -10
  17. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/web/src/core/auth.py +30 -30
  18. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/web/src/core/cache.py +14 -14
  19. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/web/src/core/config.py +7 -4
  20. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/web/src/core/credential_store.py +24 -21
  21. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/web/src/core/security.py +2 -0
  22. qqmusic_api_python-0.6.2/web/src/modules/comment.py +24 -0
  23. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/web/src/modules/login.py +28 -5
  24. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/web/src/modules/mv.py +1 -1
  25. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/web/src/modules/singer.py +1 -1
  26. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/web/src/modules/song.py +15 -4
  27. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/web/src/modules/songlist.py +1 -1
  28. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/web/src/routes/_helpers.py +9 -4
  29. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/web/src/routes/comment.py +33 -3
  30. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/web/src/routes/recommend.py +8 -2
  31. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/web/src/routing/params.py +27 -3
  32. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/web/src/routing/route_types.py +1 -1
  33. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/web/src/routing/router_factory.py +2 -1
  34. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/.agents/skills/pydantic/SKILL.md +0 -0
  35. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/.agents/skills/python-standards/SKILL.md +0 -0
  36. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/.agents/skills/tarsio/SKILL.md +0 -0
  37. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/.agents/skills/tarsio/references/api-reference.md +0 -0
  38. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/.agents/skills/uv-package-manager/SKILL.md +0 -0
  39. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/.dockerignore +0 -0
  40. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/.github/ISSUE_TEMPLATE/bug.yml +0 -0
  41. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  42. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/.github/ISSUE_TEMPLATE/feature.yml +0 -0
  43. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  44. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/.github/renovate.json +0 -0
  45. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/.github/workflows/checking.yaml +0 -0
  46. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/.github/workflows/docs.yml +0 -0
  47. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/.github/workflows/release.yml +0 -0
  48. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/.github/workflows/testing.yml +0 -0
  49. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/.gitignore +0 -0
  50. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/.markdownlint-cli2.yaml +0 -0
  51. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/AGENTS.md +0 -0
  52. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/CLAUDE.md +0 -0
  53. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/LICENSE +0 -0
  54. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/README.md +0 -0
  55. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/assets/qq-music.svg +0 -0
  56. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/cliff.toml +0 -0
  57. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/docs/coding.md +0 -0
  58. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/docs/contributing.md +0 -0
  59. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/docs/index.md +0 -0
  60. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/docs/reference/core/client.md +0 -0
  61. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/docs/reference/core/exception.md +0 -0
  62. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/docs/reference/core/pagination.md +0 -0
  63. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/docs/reference/core/request.md +0 -0
  64. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/docs/reference/core/versioning.md +0 -0
  65. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/docs/reference/model/album.md +0 -0
  66. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/docs/reference/model/base.md +0 -0
  67. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/docs/reference/model/comment.md +0 -0
  68. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/docs/reference/model/login.md +0 -0
  69. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/docs/reference/model/lyric.md +0 -0
  70. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/docs/reference/model/mv.md +0 -0
  71. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/docs/reference/model/recommend.md +0 -0
  72. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/docs/reference/model/request.md +0 -0
  73. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/docs/reference/model/search.md +0 -0
  74. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/docs/reference/model/singer.md +0 -0
  75. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/docs/reference/model/song.md +0 -0
  76. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/docs/reference/model/songlist.md +0 -0
  77. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/docs/reference/model/top.md +0 -0
  78. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/docs/reference/model/user.md +0 -0
  79. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/docs/reference/modules/album.md +0 -0
  80. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/docs/reference/modules/comment.md +0 -0
  81. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/docs/reference/modules/login.md +0 -0
  82. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/docs/reference/modules/login_utils.md +0 -0
  83. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/docs/reference/modules/lyric.md +0 -0
  84. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/docs/reference/modules/mv.md +0 -0
  85. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/docs/reference/modules/private_message.md +0 -0
  86. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/docs/reference/modules/recommend.md +0 -0
  87. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/docs/reference/modules/search.md +0 -0
  88. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/docs/reference/modules/singer.md +0 -0
  89. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/docs/reference/modules/song.md +0 -0
  90. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/docs/reference/modules/songlist.md +0 -0
  91. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/docs/reference/modules/top.md +0 -0
  92. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/docs/reference/modules/user.md +0 -0
  93. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/docs/tutorial/credential.md +0 -0
  94. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/docs/tutorial/pagination.md +0 -0
  95. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/docs/tutorial/start.md +0 -0
  96. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/docs/tutorial/web.md +0 -0
  97. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/examples/download_song.py +0 -0
  98. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/examples/phone_login.py +0 -0
  99. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/examples/private_message.py +0 -0
  100. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/examples/qrcode_login.py +0 -0
  101. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/prek.toml +0 -0
  102. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/pyproject.toml +0 -0
  103. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/qqmusic_api/algorithms/__init__.py +0 -0
  104. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/qqmusic_api/algorithms/tripledes.py +0 -0
  105. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/qqmusic_api/core/__init__.py +0 -0
  106. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/qqmusic_api/core/exceptions.py +0 -0
  107. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/qqmusic_api/core/pagination.py +0 -0
  108. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/qqmusic_api/models/__init__.py +0 -0
  109. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/qqmusic_api/models/album.py +0 -0
  110. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/qqmusic_api/models/base.py +0 -0
  111. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/qqmusic_api/models/comment.py +0 -0
  112. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/qqmusic_api/models/login.py +0 -0
  113. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/qqmusic_api/models/lyric.py +0 -0
  114. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/qqmusic_api/models/mv.py +0 -0
  115. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/qqmusic_api/models/private_message.py +0 -0
  116. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/qqmusic_api/models/search.py +0 -0
  117. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/qqmusic_api/models/singer.py +0 -0
  118. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/qqmusic_api/models/song.py +0 -0
  119. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/qqmusic_api/models/songlist.py +0 -0
  120. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/qqmusic_api/models/top.py +0 -0
  121. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/qqmusic_api/models/user.py +0 -0
  122. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/qqmusic_api/modules/__init__.py +0 -0
  123. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/qqmusic_api/modules/_base.py +0 -0
  124. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/qqmusic_api/modules/album.py +0 -0
  125. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/qqmusic_api/modules/comment.py +0 -0
  126. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/qqmusic_api/modules/login_utils.py +0 -0
  127. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/qqmusic_api/modules/lyric.py +0 -0
  128. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/qqmusic_api/modules/mv.py +0 -0
  129. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/qqmusic_api/modules/private_message.py +0 -0
  130. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/qqmusic_api/modules/search.py +0 -0
  131. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/qqmusic_api/modules/singer.py +0 -0
  132. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/qqmusic_api/modules/song.py +0 -0
  133. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/qqmusic_api/modules/songlist.py +0 -0
  134. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/qqmusic_api/modules/top.py +0 -0
  135. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/qqmusic_api/modules/user.py +0 -0
  136. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/qqmusic_api/utils/__init__.py +0 -0
  137. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/qqmusic_api/utils/common.py +0 -0
  138. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/qqmusic_api/utils/mqtt.py +0 -0
  139. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/scripts/ag-1.py +0 -0
  140. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/tests/conftest.py +0 -0
  141. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/tests/test_album.py +0 -0
  142. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/tests/test_comment.py +0 -0
  143. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/tests/test_login.py +0 -0
  144. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/tests/test_login_utils.py +0 -0
  145. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/tests/test_lyric.py +0 -0
  146. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/tests/test_mv.py +0 -0
  147. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/tests/test_private_message.py +0 -0
  148. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/tests/test_recommend.py +0 -0
  149. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/tests/test_search.py +0 -0
  150. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/tests/test_singer.py +0 -0
  151. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/tests/test_song.py +0 -0
  152. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/tests/test_songlist.py +0 -0
  153. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/tests/test_top.py +0 -0
  154. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/tests/test_user.py +0 -0
  155. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/web/.gitignore +0 -0
  156. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/web/Dockerfile +0 -0
  157. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/web/README.md +0 -0
  158. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/web/accounts.example.toml +0 -0
  159. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/web/config.example.toml +0 -0
  160. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/web/docker-compose.yml +0 -0
  161. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/web/src/core/__init__.py +0 -0
  162. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/web/src/core/deps.py +0 -0
  163. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/web/src/core/response.py +0 -0
  164. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/web/src/modules/__init__.py +0 -0
  165. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/web/src/routes/__init__.py +0 -0
  166. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/web/src/routes/album.py +0 -0
  167. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/web/src/routes/login.py +0 -0
  168. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/web/src/routes/lyric.py +0 -0
  169. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/web/src/routes/mv.py +0 -0
  170. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/web/src/routes/search.py +0 -0
  171. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/web/src/routes/singer.py +0 -0
  172. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/web/src/routes/song.py +0 -0
  173. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/web/src/routes/songlist.py +0 -0
  174. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/web/src/routes/top.py +0 -0
  175. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/web/src/routes/user.py +0 -0
  176. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/web/src/routing/__init__.py +0 -0
  177. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/web/src/routing/docstrings.py +0 -0
  178. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/web/src/routing/executor.py +0 -0
  179. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/web/tests/test_web_docstrings.py +0 -0
  180. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/web/tests/test_web_enums.py +0 -0
  181. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/web/tests/test_web_route_validation.py +0 -0
  182. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/web/tests/test_web_routes.py +0 -0
  183. {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.2}/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.2
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,20 @@
1
1
 
2
+ ## [[0.6.1](https://github.com/L-1124/QQMusicApi/compare/v0.6.0..v0.6.1)] - 2026-05-20
3
+
4
+ ### Bug 修复
5
+
6
+ * 传入的platform不生效 ([b19bec5](https://github.com/L-1124/QQMusicApi/commit/b19bec52f044d6508d9506f7111fea6eee5b42de)) by [@L-1124](https://github.com/L-1124)
7
+
8
+ ### 功能更新
9
+
10
+ * **(comment)** 支持添加评论和删除评论功能 ([dc5f568](https://github.com/L-1124/QQMusicApi/commit/dc5f5685d31f48b5dd8fc6bfcfdb6fb357884e7e)) by [@L-1124](https://github.com/L-1124)
11
+ * **(private-message)** 新增私信接口模块 ([0ce6c77](https://github.com/L-1124/QQMusicApi/commit/0ce6c77b423c01c684cf86ff7a7ee9ca9a70d9fa)) by [@L-1124](https://github.com/L-1124)
12
+
13
+ ### 贡献者
14
+
15
+ * @L-1124
16
+ * @github-actions[bot]
17
+
2
18
  ## [[0.6.0](https://github.com/L-1124/QQMusicApi/compare/v0.5.3..v0.6.0)] - 2026-05-09
3
19
 
4
20
  ### 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.2"
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,
@@ -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}
@@ -10,7 +10,7 @@ from typing import Any
10
10
  from uuid import uuid4
11
11
 
12
12
  import anyio
13
- from niquests.exceptions import HTTPError, ReadTimeout
13
+ from niquests.exceptions import HTTPError, ReadTimeout, RequestException
14
14
 
15
15
  from ..core import (
16
16
  ApiDataError,
@@ -501,6 +501,8 @@ class LoginApi(ApiModule):
501
501
  )
502
502
  except ReadTimeout:
503
503
  return QRLoginResult(event=QRCodeLoginEvents.SCAN)
504
+ except RequestException as exc:
505
+ raise NetworkError(str(exc)) from exc
504
506
 
505
507
  match = _WX_STATUS_RE.search(response.text or "")
506
508
  if not match:
@@ -17,6 +17,7 @@ from ..models.recommend import (
17
17
  RecommendNewSongResponse,
18
18
  RecommendSonglistResponse,
19
19
  )
20
+ from ..models.request import Credential
20
21
  from ._base import ApiModule
21
22
 
22
23
 
@@ -82,8 +83,12 @@ class RecommendApi(ApiModule):
82
83
  ),
83
84
  )
84
85
 
85
- def get_guess_recommend(self):
86
- """获取猜你喜欢推荐."""
86
+ def get_guess_recommend(self, *, credential: Credential | None = None):
87
+ """获取猜你喜欢推荐.
88
+
89
+ Tips:
90
+ 请求平台非 `Platform.ANDROID` 时, 需要提供有效的 `Credential`.
91
+ """
87
92
  data = {
88
93
  "id": 99,
89
94
  "num": 5,
@@ -96,6 +101,7 @@ class RecommendApi(ApiModule):
96
101
  "get_radio_track",
97
102
  data,
98
103
  response_model=GuessRecommendResponse,
104
+ credential=credential,
99
105
  )
100
106
 
101
107
  def get_radar_recommend(self, page: int = 1):
@@ -85,6 +85,10 @@ class Device:
85
85
  vendor_os_name: str = "qmapi"
86
86
  qimei: str | None = None
87
87
  qimei36: str | None = None
88
+ session_uid: str | None = None
89
+ session_sid: str | None = None
90
+ session_vkey: str | None = None
91
+ open_udid: str = field(default_factory=lambda: uuid4().hex)
88
92
 
89
93
 
90
94
  class DeviceManager:
@@ -15,7 +15,6 @@ from cryptography.hazmat.primitives import serialization
15
15
  from cryptography.hazmat.primitives.asymmetric import padding
16
16
  from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
17
17
  from niquests import AsyncSession
18
- from niquests.exceptions import RequestException
19
18
 
20
19
  from .common import calc_md5
21
20
  from .device import Device, DeviceManager
@@ -30,7 +29,6 @@ MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDEIxgwoutfwoJxcGQeedgP7FG9qaIuS0qzfR8gWkrk
30
29
  -----END PUBLIC KEY-----"""
31
30
  SECRET = "ZdJqM15EeO2zWc08"
32
31
  APP_KEY = "0AND0HD6FE4HY80F"
33
- DEFAULT_QIMEI = "6c9d3cd110abca9b16311cee10001e717614"
34
32
  CHANNEL_ID = "10003505"
35
33
  PACKAGE_ID = "com.tencent.qqmusic"
36
34
  HEX_CHARS = "0123456789abcdef"
@@ -71,17 +69,12 @@ class QimeiManager:
71
69
  async with self._lock:
72
70
  if self._cache is not None:
73
71
  return self._cache
74
-
75
72
  device = await self._device_store.get_device()
76
73
  if device.qimei and device.qimei36:
77
74
  self._cache = QimeiResult(q16=device.qimei, q36=device.qimei36)
78
75
  return self._cache
79
76
 
80
- try:
81
- self._cache = await self._request_qimei(device)
82
- except Exception:
83
- self._cache = QimeiResult(q16=DEFAULT_QIMEI, q36=DEFAULT_QIMEI)
84
-
77
+ self._cache = await self._request_qimei(device)
85
78
  with contextlib.suppress(Exception):
86
79
  await self._device_store.apply_qimei(
87
80
  self._cache.get("q16") or "",
@@ -90,34 +83,38 @@ class QimeiManager:
90
83
  return self._cache
91
84
 
92
85
  async def _request_qimei(self, device: Device) -> QimeiResult:
93
- """请求新的 QIMEI 信息."""
94
- try:
95
- _, headers, request_json = await to_thread.run_sync(
96
- _build_qimei_request,
97
- device,
98
- self._app_version,
99
- self._sdk_version,
100
- )
101
-
102
- client = self._session
103
- res = await client.post(
104
- "https://api.tencentmusic.com/tme/trpc/proxy",
105
- headers=headers,
106
- json=request_json,
107
- )
108
- res.raise_for_status()
109
-
110
- if res.content is None:
111
- return QimeiResult(q16=DEFAULT_QIMEI, q36=DEFAULT_QIMEI)
112
-
113
- qimei_data: dict[str, str] = json.loads(json.loads(res.content).get("data", "{}")).get("data", {})
114
-
115
- if not qimei_data or "q36" not in qimei_data or "q16" not in qimei_data:
116
- return QimeiResult(q16=DEFAULT_QIMEI, q36=DEFAULT_QIMEI)
117
-
118
- return QimeiResult(q16=qimei_data["q16"], q36=qimei_data["q36"])
119
- except (RequestException, json.JSONDecodeError, KeyError, ValueError):
120
- return QimeiResult(q16=DEFAULT_QIMEI, q36=DEFAULT_QIMEI)
86
+ """请求新的 QIMEI 信息.
87
+
88
+ Raises:
89
+ RuntimeError: QIMEI 服务端返回空内容或缺少必要字段时.
90
+ RequestException: 网络请求失败时.
91
+ json.JSONDecodeError: 响应解析失败时.
92
+ """
93
+ _, headers, request_json = await to_thread.run_sync(
94
+ _build_qimei_request,
95
+ device,
96
+ self._app_version,
97
+ self._sdk_version,
98
+ )
99
+
100
+ client = self._session
101
+ res = await client.post(
102
+ "https://api.tencentmusic.com/tme/trpc/proxy",
103
+ headers=headers,
104
+ json=request_json,
105
+ )
106
+ await self._session.gather(res)
107
+ res.raise_for_status()
108
+
109
+ if res.content is None:
110
+ raise RuntimeError("QIMEI response content is empty")
111
+
112
+ qimei_data: dict[str, str] = json.loads(json.loads(res.content).get("data", "{}")).get("data", {})
113
+
114
+ if not qimei_data or "q36" not in qimei_data or "q16" not in qimei_data:
115
+ raise RuntimeError(f"QIMEI response missing required fields: {qimei_data}")
116
+
117
+ return QimeiResult(q16=qimei_data["q16"], q36=qimei_data["q36"])
121
118
 
122
119
 
123
120
  def rsa_encrypt(content: bytes) -> bytes: