qqmusic-api-python 0.6.5__tar.gz → 0.6.7__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.
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/.github/workflows/docs.yml +4 -2
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/AGENTS.md +0 -1
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/PKG-INFO +1 -1
- qqmusic_api_python-0.6.7/docs/coding.md +428 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/docs/release-notes.md +60 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/docs/tutorial/client.md +13 -6
- qqmusic_api_python-0.6.7/docs/tutorial/download.md +124 -0
- qqmusic_api_python-0.6.7/docs/tutorial/error-handling.md +69 -0
- qqmusic_api_python-0.6.7/docs/tutorial/login.md +171 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/docs/tutorial/start.md +16 -12
- qqmusic_api_python-0.6.7/examples/get_all_sheets.py +47 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/qqmusic_api/__init__.py +9 -1
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/qqmusic_api/algorithms/__init__.py +2 -0
- qqmusic_api_python-0.6.7/qqmusic_api/algorithms/sign.py +32 -0
- qqmusic_api_python-0.6.7/qqmusic_api/algorithms/tripledes.py +1038 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/qqmusic_api/core/__init__.py +8 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/qqmusic_api/core/client.py +31 -13
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/qqmusic_api/core/exceptions.py +69 -1
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/qqmusic_api/core/request.py +8 -1
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/qqmusic_api/models/base.py +29 -29
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/qqmusic_api/models/request.py +1 -1
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/qqmusic_api/models/song.py +23 -2
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/qqmusic_api/modules/_base.py +66 -1
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/qqmusic_api/modules/comment.py +4 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/qqmusic_api/modules/login.py +27 -5
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/qqmusic_api/modules/private_message.py +4 -1
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/qqmusic_api/modules/search.py +2 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/qqmusic_api/modules/singer.py +2 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/qqmusic_api/modules/song.py +65 -2
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/qqmusic_api/utils/device.py +3 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/qqmusic_api/utils/qimei.py +12 -4
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/tests/test_login.py +49 -5
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/tests/test_lyric.py +6 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/web/Dockerfile +5 -2
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/web/README.md +14 -2
- qqmusic_api_python-0.6.7/web/requirements.txt +1508 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/web/src/app.py +8 -2
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/web/src/core/auth.py +11 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/web/src/core/security.py +10 -2
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/web/src/modules/comment.py +1 -1
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/web/src/routes/song.py +2 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/web/src/routing/executor.py +12 -12
- qqmusic_api_python-0.6.7/web/tests/test_web_core.py +85 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/zensical.toml +8 -3
- qqmusic_api_python-0.6.5/docs/coding.md +0 -342
- qqmusic_api_python-0.6.5/qqmusic_api/algorithms/tripledes.py +0 -598
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/.agents/skills/pydantic/SKILL.md +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/.agents/skills/python-standards/SKILL.md +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/.agents/skills/tarsio/SKILL.md +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/.agents/skills/tarsio/references/api-reference.md +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/.agents/skills/uv-package-manager/SKILL.md +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/.dockerignore +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/.github/ISSUE_TEMPLATE/bug.yml +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/.github/ISSUE_TEMPLATE/feature.yml +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/.github/renovate.json +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/.github/workflows/checking.yaml +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/.github/workflows/release.yml +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/.github/workflows/testing.yml +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/.gitignore +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/.markdownlint-cli2.yaml +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/CLAUDE.md +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/LICENSE +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/README.md +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/assets/qq-music.svg +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/cliff.toml +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/docs/contributing.md +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/docs/index.md +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/docs/reference/core/client.md +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/docs/reference/core/exception.md +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/docs/reference/core/pagination.md +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/docs/reference/core/request.md +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/docs/reference/core/versioning.md +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/docs/reference/model/album.md +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/docs/reference/model/base.md +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/docs/reference/model/comment.md +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/docs/reference/model/login.md +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/docs/reference/model/lyric.md +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/docs/reference/model/mv.md +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/docs/reference/model/recommend.md +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/docs/reference/model/request.md +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/docs/reference/model/search.md +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/docs/reference/model/singer.md +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/docs/reference/model/song.md +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/docs/reference/model/songlist.md +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/docs/reference/model/top.md +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/docs/reference/model/user.md +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/docs/reference/modules/album.md +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/docs/reference/modules/comment.md +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/docs/reference/modules/login.md +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/docs/reference/modules/login_utils.md +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/docs/reference/modules/lyric.md +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/docs/reference/modules/mv.md +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/docs/reference/modules/private_message.md +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/docs/reference/modules/recommend.md +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/docs/reference/modules/search.md +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/docs/reference/modules/singer.md +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/docs/reference/modules/song.md +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/docs/reference/modules/songlist.md +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/docs/reference/modules/top.md +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/docs/reference/modules/user.md +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/docs/tutorial/credential.md +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/docs/tutorial/pagination.md +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/docs/tutorial/web.md +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/examples/download_song.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/examples/phone_login.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/examples/private_message.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/examples/qrcode_login.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/prek.toml +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/pyproject.toml +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/qqmusic_api/core/pagination.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/qqmusic_api/core/versioning.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/qqmusic_api/models/__init__.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/qqmusic_api/models/_validator.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/qqmusic_api/models/album.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/qqmusic_api/models/comment.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/qqmusic_api/models/login.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/qqmusic_api/models/lyric.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/qqmusic_api/models/mv.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/qqmusic_api/models/private_message.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/qqmusic_api/models/recommend.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/qqmusic_api/models/search.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/qqmusic_api/models/singer.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/qqmusic_api/models/songlist.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/qqmusic_api/models/top.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/qqmusic_api/models/user.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/qqmusic_api/modules/__init__.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/qqmusic_api/modules/album.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/qqmusic_api/modules/login_utils.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/qqmusic_api/modules/lyric.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/qqmusic_api/modules/mv.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/qqmusic_api/modules/recommend.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/qqmusic_api/modules/songlist.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/qqmusic_api/modules/top.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/qqmusic_api/modules/user.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/qqmusic_api/utils/__init__.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/qqmusic_api/utils/common.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/qqmusic_api/utils/mqtt.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/scripts/ag-1.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/tests/conftest.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/tests/test_album.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/tests/test_comment.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/tests/test_login_utils.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/tests/test_mv.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/tests/test_private_message.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/tests/test_recommend.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/tests/test_search.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/tests/test_singer.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/tests/test_song.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/tests/test_songlist.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/tests/test_top.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/tests/test_user.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/uv.lock +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/web/.gitignore +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/web/accounts.example.toml +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/web/config.example.toml +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/web/docker-compose.yml +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/web/run.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/web/src/core/__init__.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/web/src/core/cache.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/web/src/core/config.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/web/src/core/credential_store.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/web/src/core/deps.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/web/src/core/response.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/web/src/modules/__init__.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/web/src/modules/login.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/web/src/modules/mv.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/web/src/modules/singer.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/web/src/modules/song.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/web/src/modules/songlist.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/web/src/routes/__init__.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/web/src/routes/_helpers.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/web/src/routes/album.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/web/src/routes/comment.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/web/src/routes/login.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/web/src/routes/lyric.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/web/src/routes/mv.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/web/src/routes/recommend.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/web/src/routes/search.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/web/src/routes/singer.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/web/src/routes/songlist.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/web/src/routes/top.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/web/src/routes/user.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/web/src/routing/__init__.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/web/src/routing/docstrings.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/web/src/routing/params.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/web/src/routing/route_types.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/web/src/routing/router_factory.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/web/tests/test_web_docstrings.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/web/tests/test_web_enums.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/web/tests/test_web_route_validation.py +0 -0
- {qqmusic_api_python-0.6.5 → qqmusic_api_python-0.6.7}/web/tests/test_web_routes.py +0 -0
|
@@ -41,14 +41,16 @@ jobs:
|
|
|
41
41
|
env:
|
|
42
42
|
OUTPUT: docs/release-notes.md
|
|
43
43
|
GITHUB_REPO: ${{ github.repository }}
|
|
44
|
+
- name: Export web requirements.txt
|
|
45
|
+
run: uv export --format requirements.txt --group web --output-file web/requirements.txt
|
|
44
46
|
- name: Commit
|
|
45
47
|
run: |
|
|
46
48
|
git checkout main
|
|
47
49
|
git config user.name "github-actions[bot]"
|
|
48
50
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
49
51
|
set +e
|
|
50
|
-
git add docs/release-notes.md
|
|
51
|
-
git commit -m "🧹 chore(release
|
|
52
|
+
git add docs/release-notes.md web/requirements.txt
|
|
53
|
+
git commit -m "🧹 chore(release): 更新 release notes 及 web 依赖"
|
|
52
54
|
git push origin main
|
|
53
55
|
- name: Build with zensical
|
|
54
56
|
run: uv run zensical build
|
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
# API 编写指南
|
|
2
|
+
|
|
3
|
+
`qqmusic_api` 采用 `Client + ApiModule + Request` 的结构:
|
|
4
|
+
|
|
5
|
+
* `Client` 负责网络发送、平台信息和凭证。
|
|
6
|
+
* `ApiModule` 负责声明接口参数,并返回可 `await` 的 `Request`。
|
|
7
|
+
|
|
8
|
+
## 调用流程图
|
|
9
|
+
|
|
10
|
+
### 单请求
|
|
11
|
+
|
|
12
|
+
```text
|
|
13
|
+
模块方法
|
|
14
|
+
-> self._build_request(...)
|
|
15
|
+
-> Request
|
|
16
|
+
-> await request
|
|
17
|
+
-> Client.execute(request)
|
|
18
|
+
-> Client.request_api(...) (根据 request.is_jce 分发改用 JCE 或 JSON 协议)
|
|
19
|
+
-> Client._build_result(...)
|
|
20
|
+
-> 返回原始 dict / TarsDict 或 Pydantic 模型
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### 批量并发请求
|
|
24
|
+
|
|
25
|
+
```text
|
|
26
|
+
多个模块方法
|
|
27
|
+
-> self._build_request(...)
|
|
28
|
+
-> Request 列表
|
|
29
|
+
-> Client.gather(requests)
|
|
30
|
+
-> 按协议、平台、公共参数和凭证分组
|
|
31
|
+
-> 每组按 batch_size 拆分为批量请求
|
|
32
|
+
-> 依次调用 Client.request_api(..., lazy=True) 生成响应任务
|
|
33
|
+
-> 使用客户端内部的 multiplexed AsyncSession 并发执行这些任务(self._session.gather)
|
|
34
|
+
-> 按 req_n 解析每个响应项
|
|
35
|
+
-> 按输入顺序返回结果
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`gather` 的分组边界由 `Request._group_key` 决定。只有协议类型、显式平台、公共参数和凭证相同的请求才会合并到同一个批量请求中。
|
|
39
|
+
|
|
40
|
+
## 编写新的 API
|
|
41
|
+
|
|
42
|
+
### 添加新模块
|
|
43
|
+
|
|
44
|
+
1. 在 `qqmusic_api/modules/` 下创建新文件,例如 `foo.py`。
|
|
45
|
+
2. 定义模块类,继承 `ApiModule`。
|
|
46
|
+
3. 在 `Client` 中注册为 `@cached_property`。
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
# qqmusic_api/modules/foo.py
|
|
50
|
+
from ._base import ApiModule
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class FooApi(ApiModule):
|
|
54
|
+
"""Foo 相关 API."""
|
|
55
|
+
|
|
56
|
+
def get_something(self, id: int):
|
|
57
|
+
"""获取某项数据."""
|
|
58
|
+
return self._build_request(
|
|
59
|
+
module="music.foo.Svc",
|
|
60
|
+
method="GetSomething",
|
|
61
|
+
param={"id": id},
|
|
62
|
+
)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
# qqmusic_api/core/client.py
|
|
67
|
+
from functools import cached_property
|
|
68
|
+
|
|
69
|
+
class Client:
|
|
70
|
+
@cached_property
|
|
71
|
+
def foo(self) -> "FooApi":
|
|
72
|
+
from ..modules.foo import FooApi
|
|
73
|
+
return FooApi(self)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### 添加新的请求方法
|
|
77
|
+
|
|
78
|
+
API 方法返回 `Request` 对象,不直接发起请求。使用 `self._build_request(...)` 工厂方法构建:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
def get_detail(self, song_id: int):
|
|
82
|
+
"""获取歌曲详情."""
|
|
83
|
+
return self._build_request(
|
|
84
|
+
module="music.songDetail", # 接口所属模块
|
|
85
|
+
method="GetDetail", # 方法名
|
|
86
|
+
param={"songid": song_id}, # 业务参数
|
|
87
|
+
)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
对于非标准 CGI 接口(如直接 GET 请求),使用 `self._request(...)`:
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
async def quick_search(self, keyword: str) -> dict[str, Any]:
|
|
94
|
+
"""快速搜索 (直接返回解析后的 JSON 数据)."""
|
|
95
|
+
resp = await self._request(
|
|
96
|
+
"GET",
|
|
97
|
+
"https://c.y.qq.com/splcloud/fcgi-bin/smartbox_new.fcg",
|
|
98
|
+
params={"key": keyword},
|
|
99
|
+
)
|
|
100
|
+
resp.raise_for_status()
|
|
101
|
+
return resp.json()["data"]
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### `_build_request` 参数说明
|
|
105
|
+
|
|
106
|
+
| 参数 | 类型 | 说明 |
|
|
107
|
+
|------------------|-----------------------------|-----------------------------------------------------------|
|
|
108
|
+
| `module` | `str` | 接口所属模块名 |
|
|
109
|
+
| `method` | `str` | 方法名 |
|
|
110
|
+
| `param` | `dict` | 业务参数 |
|
|
111
|
+
| `response_model` | `type[BaseModel]` 或 `None` | 响应模型,为 None 时返回原始 dict |
|
|
112
|
+
| `comm` | `dict` 或 `None` | 附加的公共参数 |
|
|
113
|
+
| `override_comm` | `bool` | 为 True 时 `comm` 完全替代自动生成的参数;为 False 时合并 |
|
|
114
|
+
| `credential` | `Credential` 或 `None` | 覆盖本次请求的凭证 |
|
|
115
|
+
| `platform` | `Platform` 或 `None` | 覆盖本次请求的平台 |
|
|
116
|
+
| `is_jce` | `bool` | 是否作为 JCE (Tars) 二进制协议发送 |
|
|
117
|
+
| `preserve_bool` | `bool` | 是否保留布尔值原样(默认转为 0/1 整型) |
|
|
118
|
+
| `sign` | `bool` | 是否对请求进行签名 |
|
|
119
|
+
| `pager_meta` | `PagerMeta` 或 `None` | 分页元数据,提供后返回 `PaginatedRequest` |
|
|
120
|
+
| `refresh_meta` | `RefreshMeta` 或 `None` | 换一批元数据,提供后返回 `RefreshableRequest` |
|
|
121
|
+
|
|
122
|
+
### `client.request` 参数说明
|
|
123
|
+
|
|
124
|
+
`client.request` 是底层 HTTP 请求方法,自动装配凭证 Cookies 和平台 User-Agent:
|
|
125
|
+
|
|
126
|
+
| 参数 | 类型 | 说明 |
|
|
127
|
+
|--------------|------------------------|----------------------------------------------------------------------------------:|
|
|
128
|
+
| `method` | `str` | HTTP 方法,如 `"GET"`、`"POST"` |
|
|
129
|
+
| `url` | `str` | 请求地址 |
|
|
130
|
+
| `credential` | `Credential` 或 `None` | 覆盖本次请求的凭证,默认使用客户端凭证 |
|
|
131
|
+
| `platform` | `Platform` 或 `None` | 覆盖本次请求的平台,默认使用客户端平台 |
|
|
132
|
+
| `lazy` | `bool` | 是否延迟发送请求(用于批量并发) |
|
|
133
|
+
| `**kwargs` | | 透传给底层 `niquests` 的参数(`params`、`json`、`data`、`headers`、`cookies` 等) |
|
|
134
|
+
|
|
135
|
+
!!! note
|
|
136
|
+
|
|
137
|
+
`client.request` 返回的是原始 `niquests.Response` 对象,需要手动解析响应。而 `_build_request` 返回的 `Request` 对象支持 `await`,会自动完成响应验证和模型解析。
|
|
138
|
+
|
|
139
|
+
常见用法:
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
# GET 请求
|
|
143
|
+
resp = await client.request("GET", "https://example.com/api", params={"key": "value"})
|
|
144
|
+
|
|
145
|
+
# POST JSON
|
|
146
|
+
resp = await client.request("POST", "https://example.com/api", json={"key": "value"})
|
|
147
|
+
|
|
148
|
+
# POST form data
|
|
149
|
+
resp = await client.request("POST", "https://example.com/api", data={"key": "value"})
|
|
150
|
+
|
|
151
|
+
# 自定义 headers
|
|
152
|
+
resp = await client.request("GET", "https://example.com/api", headers={"X-Custom": "value"})
|
|
153
|
+
|
|
154
|
+
# 覆盖凭证
|
|
155
|
+
resp = await client.request("GET", "https://example.com/api", credential=my_credential)
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## 响应模型
|
|
159
|
+
|
|
160
|
+
### 基础用法
|
|
161
|
+
|
|
162
|
+
每个响应模型都应继承 `models.request.Response`:
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
from pydantic import Field
|
|
166
|
+
|
|
167
|
+
from .request import Response
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class MyResponse(Response):
|
|
171
|
+
"""我的响应模型."""
|
|
172
|
+
|
|
173
|
+
name: str
|
|
174
|
+
count: int
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
`Response` 基类配置了 `frozen=True`(不可变)和 `extra="ignore"`(忽略多余字段)。
|
|
178
|
+
|
|
179
|
+
### JSONPath 字段映射
|
|
180
|
+
|
|
181
|
+
可以通过 `Field(json_schema_extra={"jsonpath": ...})` 声明字段的 JSONPath 映射路径,自动从嵌套响应中提取数据:
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
class SonglistMeta(Response):
|
|
185
|
+
"""歌单元数据示例."""
|
|
186
|
+
|
|
187
|
+
id: int = Field(json_schema_extra={"jsonpath": "$.result.tid"})
|
|
188
|
+
dirid: int = Field(json_schema_extra={"jsonpath": "$.result.dirId"})
|
|
189
|
+
name: str = Field(json_schema_extra={"jsonpath": "$.result.dirName"})
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
对于列表字段,使用 `[*]` 通配符:
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
class CommentListResponse(Response):
|
|
196
|
+
"""评论列表响应."""
|
|
197
|
+
|
|
198
|
+
comments: list[Comment] = Field(
|
|
199
|
+
default_factory=list,
|
|
200
|
+
json_schema_extra={"jsonpath": "$.commentlist[*]"},
|
|
201
|
+
)
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### 字段别名
|
|
205
|
+
|
|
206
|
+
Pydantic 的 `validation_alias` 支持多别名兼容:
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
class Singer(Response):
|
|
210
|
+
"""歌手信息."""
|
|
211
|
+
|
|
212
|
+
id: int = Field(
|
|
213
|
+
default=-1,
|
|
214
|
+
validation_alias=AliasChoices("id", "singerID", "singerId", "SingerID"),
|
|
215
|
+
)
|
|
216
|
+
mid: str = Field(
|
|
217
|
+
default="",
|
|
218
|
+
validation_alias=AliasChoices("mid", "singerMid", "singerMID"),
|
|
219
|
+
)
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### 需登录的接口
|
|
223
|
+
|
|
224
|
+
需要登录的接口应通过 `_require_login` 校验凭证:
|
|
225
|
+
|
|
226
|
+
```python
|
|
227
|
+
def get_vip_info(self, *, credential: Credential | None = None):
|
|
228
|
+
"""获取 VIP 信息."""
|
|
229
|
+
target_credential = self._require_login(credential)
|
|
230
|
+
return self._build_request(
|
|
231
|
+
module="VipLogin.VipLoginInter",
|
|
232
|
+
method="vip_login_base",
|
|
233
|
+
param={},
|
|
234
|
+
credential=target_credential,
|
|
235
|
+
response_model=UserVipInfoResponse,
|
|
236
|
+
)
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## 翻页与换一批
|
|
240
|
+
|
|
241
|
+
### 连续翻页
|
|
242
|
+
|
|
243
|
+
通过 `pager_meta` 声明连续翻页能力,返回的请求对象会暴露 `.paginate()`:
|
|
244
|
+
|
|
245
|
+
```python
|
|
246
|
+
from ..core.pagination import OffsetStrategy, PagerMeta, ResponseAdapter
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def get_detail(self, songlist_id: int, num: int = 10, page: int = 1):
|
|
250
|
+
"""获取歌单详情."""
|
|
251
|
+
return self._build_request(
|
|
252
|
+
module="music.srfDissInfo.DissInfo",
|
|
253
|
+
method="CgiGetDiss",
|
|
254
|
+
param={
|
|
255
|
+
"disstid": songlist_id,
|
|
256
|
+
"song_begin": num * (page - 1),
|
|
257
|
+
"song_num": num,
|
|
258
|
+
},
|
|
259
|
+
response_model=GetSonglistDetailResponse,
|
|
260
|
+
pager_meta=PagerMeta(
|
|
261
|
+
strategy=OffsetStrategy(offset_key="song_begin", page_size_key="song_num"),
|
|
262
|
+
adapter=ResponseAdapter(
|
|
263
|
+
has_more_flag="hasmore",
|
|
264
|
+
total="total",
|
|
265
|
+
count=lambda response: len(response.songs),
|
|
266
|
+
),
|
|
267
|
+
),
|
|
268
|
+
)
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### 换一批
|
|
272
|
+
|
|
273
|
+
通过 `refresh_meta` 声明换一批能力,返回的请求对象会暴露 `.refresh()`:
|
|
274
|
+
|
|
275
|
+
```python
|
|
276
|
+
from ..core.pagination import BatchRefreshStrategy, RefreshMeta, ResponseAdapter
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def get_related_mv(self, songid: int, last_mvid: str | None = None):
|
|
280
|
+
"""获取歌曲相关 MV."""
|
|
281
|
+
return self._build_request(
|
|
282
|
+
module="MvService.MvInfoProServer",
|
|
283
|
+
method="GetSongRelatedMv",
|
|
284
|
+
param={"songid": str(songid), "songtype": 1, "lastmvid": last_mvid or 0},
|
|
285
|
+
response_model=GetRelatedMvResponse,
|
|
286
|
+
refresh_meta=RefreshMeta(
|
|
287
|
+
strategy=BatchRefreshStrategy(refresh_key="lastmvid"),
|
|
288
|
+
adapter=ResponseAdapter(
|
|
289
|
+
has_more_flag="has_more",
|
|
290
|
+
cursor=lambda response: response.mv[-1].id if response.mv else None,
|
|
291
|
+
),
|
|
292
|
+
),
|
|
293
|
+
)
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### 内置策略速查
|
|
297
|
+
|
|
298
|
+
| 策略 | 适用场景 | 关键参数 |
|
|
299
|
+
|----------------------------------|-------------:|---------------------------------|
|
|
300
|
+
| `PageStrategy` | 页码递增 | `page_key` |
|
|
301
|
+
| `OffsetStrategy` | 偏移量滑窗 | `offset_key` + `page_size_key` |
|
|
302
|
+
| `CursorStrategy` | 响应游标回写 | `cursor_key` |
|
|
303
|
+
| `MultiFieldContinuationStrategy` | 多字段续翻 | 自定义 `build_next_params` 函数 |
|
|
304
|
+
| `BatchRefreshStrategy` | 换一批 | `refresh_key` |
|
|
305
|
+
|
|
306
|
+
`pager_meta` 与 `refresh_meta` 不能同时声明。
|
|
307
|
+
|
|
308
|
+
## JCE (Tars) 协议
|
|
309
|
+
|
|
310
|
+
部分接口使用 JCE 二进制协议而非 JSON。通过 `is_jce=True` 启用:
|
|
311
|
+
|
|
312
|
+
```python
|
|
313
|
+
def get_something(self):
|
|
314
|
+
"""获取数据 (JCE 协议)."""
|
|
315
|
+
return self._build_request(
|
|
316
|
+
module="music.foo.Svc",
|
|
317
|
+
method="GetSomething",
|
|
318
|
+
param={0: "value"}, # JCE 使用整数 key
|
|
319
|
+
is_jce=True,
|
|
320
|
+
)
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
JCE 协议的响应会自动解码为 `TarsDict`。
|
|
324
|
+
|
|
325
|
+
## 请求签名
|
|
326
|
+
|
|
327
|
+
部分接口需要对请求体进行签名。通过 `sign=True` 启用:
|
|
328
|
+
|
|
329
|
+
```python
|
|
330
|
+
def get_sheet(self, mid: str):
|
|
331
|
+
"""获取曲谱."""
|
|
332
|
+
return self._build_request(
|
|
333
|
+
module="music.mir.SheetMusicSvr",
|
|
334
|
+
method="GetMoreSheetMusic",
|
|
335
|
+
param={"songMid": mid},
|
|
336
|
+
sign=True,
|
|
337
|
+
)
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
签名后请求会发送到 `musics.fcg` 而非 `musicu.fcg`,并在 URL 参数中附加 `_`(时间戳)和 `sign`。
|
|
341
|
+
|
|
342
|
+
## 公共参数 `comm`
|
|
343
|
+
|
|
344
|
+
默认情况下,`comm` 参数由 `VersionPolicy.build_comm()` 自动生成。可以通过 `comm` 附加额外参数:
|
|
345
|
+
|
|
346
|
+
```python
|
|
347
|
+
# 合并到自动生成的 comm 中(默认行为)
|
|
348
|
+
self._build_request(
|
|
349
|
+
...,
|
|
350
|
+
comm={"extra_key": "value"},
|
|
351
|
+
)
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
使用 `override_comm=True` 完全替代自动生成的参数:
|
|
355
|
+
|
|
356
|
+
```python
|
|
357
|
+
self._build_request(
|
|
358
|
+
...,
|
|
359
|
+
comm={
|
|
360
|
+
"g_tk": 5381,
|
|
361
|
+
"uin": "",
|
|
362
|
+
"format": "json",
|
|
363
|
+
"inCharset": "utf-8",
|
|
364
|
+
"outCharset": "utf-8",
|
|
365
|
+
"notice": 0,
|
|
366
|
+
"needNewCode": 1,
|
|
367
|
+
},
|
|
368
|
+
override_comm=True,
|
|
369
|
+
)
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
## 编写测试
|
|
373
|
+
|
|
374
|
+
测试文件放在 `tests/` 下,按模块命名(如 `test_song.py`)。
|
|
375
|
+
|
|
376
|
+
### 基本格式
|
|
377
|
+
|
|
378
|
+
```python
|
|
379
|
+
"""歌曲模块测试."""
|
|
380
|
+
|
|
381
|
+
import pytest
|
|
382
|
+
|
|
383
|
+
from qqmusic_api import Client
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
async def test_query_song(client: Client) -> None:
|
|
387
|
+
"""测试根据 ID 查询歌曲."""
|
|
388
|
+
result = await client.song.query_song(["003w2xz20QlUZt"])
|
|
389
|
+
assert result.tracks
|
|
390
|
+
assert result.tracks[0].name
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
### 使用 parametrize
|
|
394
|
+
|
|
395
|
+
```python
|
|
396
|
+
@pytest.mark.parametrize("page", [1, 2])
|
|
397
|
+
async def test_general_search(client: Client, page: int) -> None:
|
|
398
|
+
"""测试综合搜索翻页."""
|
|
399
|
+
result = await client.search.general_search("周杰伦", page=page)
|
|
400
|
+
assert result.song.items is not None
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
### 需要登录的测试
|
|
404
|
+
|
|
405
|
+
使用 `authenticated_client` fixture:
|
|
406
|
+
|
|
407
|
+
```python
|
|
408
|
+
async def test_get_vip_info(authenticated_client: Client) -> None:
|
|
409
|
+
"""测试获取 VIP 信息."""
|
|
410
|
+
result = await authenticated_client.user.get_vip_info()
|
|
411
|
+
assert result.vip_flag is not None
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
### 测试分页
|
|
415
|
+
|
|
416
|
+
```python
|
|
417
|
+
async def test_search_paginate(client: Client) -> None:
|
|
418
|
+
"""测试搜索分页."""
|
|
419
|
+
pager = client.search.search_by_type("周杰伦", num=5).paginate(limit=2)
|
|
420
|
+
|
|
421
|
+
assert pager.has_more() is True
|
|
422
|
+
first_page = await pager.next()
|
|
423
|
+
assert pager.has_more() is True
|
|
424
|
+
second_page = await pager.next()
|
|
425
|
+
|
|
426
|
+
assert first_page.song
|
|
427
|
+
assert second_page.song
|
|
428
|
+
```
|
|
@@ -1,4 +1,64 @@
|
|
|
1
1
|
|
|
2
|
+
## What's Changed
|
|
3
|
+
|
|
4
|
+
### 功能更新
|
|
5
|
+
|
|
6
|
+
* **(web)** 添加 has_sheet 路由 ([9e7a143](https://github.com/L-1124/QQMusicApi/commit/9e7a1439ff597ae43a4ef99d88ec08ee32889b9a)) by [@L-1124](https://github.com/L-1124)
|
|
7
|
+
|
|
8
|
+
### 文档更新
|
|
9
|
+
|
|
10
|
+
* 完善快速开始文档并新增多篇核心教程 ([d6143cd](https://github.com/L-1124/QQMusicApi/commit/d6143cdc047ebeb4963b061680a21103ba3d58d4)) by [@L-1124](https://github.com/L-1124)
|
|
11
|
+
|
|
12
|
+
### 贡献者
|
|
13
|
+
|
|
14
|
+
* @L-1124
|
|
15
|
+
* @github-actions[bot]
|
|
16
|
+
|
|
17
|
+
## [[0.6.6](https://github.com/L-1124/QQMusicApi/compare/v0.6.5..v0.6.6)] - 2026-06-13
|
|
18
|
+
|
|
19
|
+
### Bug 修复
|
|
20
|
+
|
|
21
|
+
* **(song)** 修复 get_sheet 获取无曲谱歌曲时触发异常的问题 ([f47a643](https://github.com/L-1124/QQMusicApi/commit/f47a643e956ef52a532b0017ea8365cb2537593d)) by [@L-1124](https://github.com/L-1124)
|
|
22
|
+
* **(web)** 修复评论适配器与凭证锁内存泄漏,优化基础设施 ([66a6cc4](https://github.com/L-1124/QQMusicApi/commit/66a6cc4901b1acd99b0c4541f88a1d8e0a9b0888)) by [@L-1124](https://github.com/L-1124)
|
|
23
|
+
|
|
24
|
+
### Revert
|
|
25
|
+
|
|
26
|
+
* **(models)** 回退 BaseModel 额外字段处理策略为 extra='ignore' ([f6691e0](https://github.com/L-1124/QQMusicApi/commit/f6691e078b87a45f5178d7a018f9a1bd0d5e391a)) by [@L-1124](https://github.com/L-1124)
|
|
27
|
+
|
|
28
|
+
### 功能更新
|
|
29
|
+
|
|
30
|
+
* **(client)** 支持 zzc 请求签名 ([eb4eb66](https://github.com/L-1124/QQMusicApi/commit/eb4eb66f5c20c2adbe430bbfb11140d0431d48d5)) by [@L-1124](https://github.com/L-1124)
|
|
31
|
+
* **(core)** 支持在使用 allow_error_codes 放行时强制解析模型 ([fac2a71](https://github.com/L-1124/QQMusicApi/commit/fac2a71bfb2f2ea700d59215a439fc65b40461f2)) by [@L-1124](https://github.com/L-1124)
|
|
32
|
+
* **(core)** 支持通过 override_comm 彻底替代通用请求载荷 ([72edb4f](https://github.com/L-1124/QQMusicApi/commit/72edb4f3cc96a063643f2d58b92d41722ad95e8f)) by [@L-1124](https://github.com/L-1124)
|
|
33
|
+
* **(song)** 添加新的曲谱相关 API和相关示例 ([9c461db](https://github.com/L-1124/QQMusicApi/commit/9c461dbdff15acf804ae047a7bc827cd6b1e7dd5)) by [@L-1124](https://github.com/L-1124)
|
|
34
|
+
* **(song)** 支持 HasSheetMusic 接口检查歌曲是否有曲谱 ([94723ff](https://github.com/L-1124/QQMusicApi/commit/94723ffda911f74d680f7f661e9436bba74c3612)) by [@L-1124](https://github.com/L-1124)
|
|
35
|
+
* **(web)** 补充 requirements.txt ([d806214](https://github.com/L-1124/QQMusicApi/commit/d80621477635f6e0ad3589116cbb97c75e31dc6c)) by [@L-1124](https://github.com/L-1124)
|
|
36
|
+
|
|
37
|
+
### 性能优化
|
|
38
|
+
|
|
39
|
+
* **(qrc)** 优化自定义 3DES 解密性能并补充测试 ([0b808a5](https://github.com/L-1124/QQMusicApi/commit/0b808a5537643f2c3bd0ef94e5c72263ec378bb8)) by [@L-1124](https://github.com/L-1124)
|
|
40
|
+
|
|
41
|
+
### 构建配置
|
|
42
|
+
|
|
43
|
+
* **(web)** 在 Dockerfile 运行命令中添加 --no-sync 防止容器运行期同步 dev 依赖 ([cf1fbc5](https://github.com/L-1124/QQMusicApi/commit/cf1fbc5523c83b21734d82763a89aef450825674)) by [@L-1124](https://github.com/L-1124)
|
|
44
|
+
|
|
45
|
+
### 贡献者
|
|
46
|
+
|
|
47
|
+
* @L-1124
|
|
48
|
+
* @github-actions[bot]
|
|
49
|
+
|
|
50
|
+
## [[0.6.5](https://github.com/L-1124/QQMusicApi/compare/v0.6.4..v0.6.5)] - 2026-06-12
|
|
51
|
+
|
|
52
|
+
### Bug 修复
|
|
53
|
+
|
|
54
|
+
* **(core)** 开启 allow_incoming_cookies=False 防止上下请求的 cookies 污染 ([1cd1b69](https://github.com/L-1124/QQMusicApi/commit/1cd1b69338ffa2f26409480490b18b9c9080a29e)) by [@L-1124](https://github.com/L-1124)
|
|
55
|
+
* **(login)** `Platform.WEB` 的 `check_expired` 总是返回 True ([f0b4533](https://github.com/L-1124/QQMusicApi/commit/f0b4533e7c172b6cb5b0556fbd7ff13e8b20f4c7)) by [@L-1124](https://github.com/L-1124)
|
|
56
|
+
|
|
57
|
+
### 贡献者
|
|
58
|
+
|
|
59
|
+
* @L-1124
|
|
60
|
+
* @github-actions[bot]
|
|
61
|
+
|
|
2
62
|
## [[0.6.4](https://github.com/L-1124/QQMusicApi/compare/v0.6.3..v0.6.4)] - 2026-06-12
|
|
3
63
|
|
|
4
64
|
### Bug 修复
|
|
@@ -76,10 +76,6 @@ client = Client(credential=credential)
|
|
|
76
76
|
|
|
77
77
|
默认的请求平台是 `android`,如果需要可以在初始化时覆盖:
|
|
78
78
|
|
|
79
|
-
!!! note
|
|
80
|
-
|
|
81
|
-
部分 API 的请求平台是固定的,无法覆盖。
|
|
82
|
-
|
|
83
79
|
```python
|
|
84
80
|
import asyncio
|
|
85
81
|
|
|
@@ -91,10 +87,21 @@ async def main():
|
|
|
91
87
|
...
|
|
92
88
|
|
|
93
89
|
|
|
94
|
-
asyncio.run(
|
|
95
|
-
|
|
90
|
+
asyncio.run(main())
|
|
96
91
|
```
|
|
97
92
|
|
|
93
|
+
支持的平台:
|
|
94
|
+
|
|
95
|
+
| 平台 | `Platform` 值 | 说明 |
|
|
96
|
+
| ------ | -------------- | ------ |
|
|
97
|
+
| Android | `Platform.ANDROID` | 默认,大部分接口使用 |
|
|
98
|
+
| Desktop | `Platform.DESKTOP` | QQ 音乐桌面端 |
|
|
99
|
+
| Web | `Platform.WEB` | QQ 音乐网页端 |
|
|
100
|
+
|
|
101
|
+
!!! note
|
|
102
|
+
|
|
103
|
+
部分接口的请求平台是固定的,传入 `platform` 参数不会生效。例如 `get_detail` 固定使用 Web 平台,`send_authcode` 固定使用 Android 平台。
|
|
104
|
+
|
|
98
105
|
## 设备信息
|
|
99
106
|
|
|
100
107
|
可通过 `device_path` 参数指定设备信息文件的路径进行持久化存储:
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# 下载歌曲
|
|
2
|
+
|
|
3
|
+
获取歌曲文件链接需要登录凭证。基本流程:先获取 CDN 节点,再请求文件地址。
|
|
4
|
+
|
|
5
|
+
## 基本用法
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
import asyncio
|
|
9
|
+
import random
|
|
10
|
+
|
|
11
|
+
from qqmusic_api import Client, Credential
|
|
12
|
+
from qqmusic_api.modules.song import SongFileInfo
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
async def main() -> None:
|
|
16
|
+
credential = Credential(musicid=123456, musickey="Q_H_L_xxx")
|
|
17
|
+
async with Client(credential=credential) as client:
|
|
18
|
+
cdn_dispatch = await client.song.get_cdn_dispatch()
|
|
19
|
+
cdn = random.choice(cdn_dispatch.sip)
|
|
20
|
+
|
|
21
|
+
urls = await client.song.get_song_urls(
|
|
22
|
+
[SongFileInfo(mid="003w2xz20QlUZt")]
|
|
23
|
+
)
|
|
24
|
+
for info in urls.data:
|
|
25
|
+
if info.purl:
|
|
26
|
+
print(f"{info.mid}: {cdn}{info.purl}")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
asyncio.run(main())
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## 检查下载权限
|
|
33
|
+
|
|
34
|
+
通过 `query_song` 获取歌曲信息后,可从 `pay` 字段判断是否需要付费:
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
song = await client.song.query_song(["003w2xz20QlUZt"])
|
|
38
|
+
track = song.tracks[0]
|
|
39
|
+
|
|
40
|
+
# pay_play: 1=需要付费播放, 0=免费
|
|
41
|
+
# pay_down: 1=需要付费下载, 0=免费
|
|
42
|
+
# pay_month: 1=需要绿钻/付费包
|
|
43
|
+
if track.pay.pay_month == 1:
|
|
44
|
+
print("需要绿钻或付费包权限")
|
|
45
|
+
elif track.pay.pay_down == 1:
|
|
46
|
+
print("需要单独付费下载")
|
|
47
|
+
else:
|
|
48
|
+
print("可免费下载")
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
请求文件链接后,`UrlinfoItem.result` 会返回授权结果码:
|
|
52
|
+
|
|
53
|
+
| result | 含义 |
|
|
54
|
+
|----------|----------------------------|
|
|
55
|
+
| `0` | 成功,`purl` 可用 |
|
|
56
|
+
| `104003` | 无权限(未登录或等级不够) |
|
|
57
|
+
| `104004` | VKey 获取失败 |
|
|
58
|
+
| `104013` | 播放设备受限 |
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
urls = await client.song.get_song_urls([SongFileInfo(mid="003w2xz20QlUZt")])
|
|
62
|
+
for info in urls.data:
|
|
63
|
+
if info.result == 0 and info.purl:
|
|
64
|
+
url = cdn + info.purl
|
|
65
|
+
print(f"可下载: {url}")
|
|
66
|
+
else:
|
|
67
|
+
print(f"无法下载 (result={info.result})")
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## 检查文件是否存在
|
|
71
|
+
|
|
72
|
+
`query_song` 返回的 `file` 字段包含各音质的文件大小,值为 `0` 表示该音质不存在:
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
song = await client.song.query_song(["003w2xz20QlUZt"])
|
|
76
|
+
track = song.tracks[0]
|
|
77
|
+
|
|
78
|
+
file = track.file
|
|
79
|
+
if file.size_flac > 0:
|
|
80
|
+
print(f"FLAC 无损可用, 大小: {file.size_flac / 1024 / 1024:.1f} MB")
|
|
81
|
+
if file.size_320mp3 > 0:
|
|
82
|
+
print(f"MP3 320k 可用, 大小: {file.size_320mp3 / 1024 / 1024:.1f} MB")
|
|
83
|
+
if file.size_128mp3 > 0:
|
|
84
|
+
print(f"MP3 128k 可用, 大小: {file.size_128mp3 / 1024 / 1024:.1f} MB")
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## 同时获取多种音质
|
|
88
|
+
|
|
89
|
+
`get_song_urls` 支持在一次请求中为同一首歌请求不同音质。为每首歌构造多个 `SongFileInfo`,指定不同的 `file_type`:
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from qqmusic_api.modules.song import SongFileInfo, SongFileType
|
|
93
|
+
|
|
94
|
+
song_mid = "003w2xz20QlUZt"
|
|
95
|
+
media_mid = "003w2xz20QlUZt" # 从 track.file.media_mid 获取
|
|
96
|
+
|
|
97
|
+
urls = await client.song.get_song_urls(
|
|
98
|
+
[
|
|
99
|
+
SongFileInfo(mid=song_mid, file_type=SongFileType.MP3_128, media_mid=media_mid),
|
|
100
|
+
SongFileInfo(mid=song_mid, file_type=SongFileType.FLAC, media_mid=media_mid),
|
|
101
|
+
SongFileInfo(mid=song_mid, file_type=SongFileType.OGG_320, media_mid=media_mid),
|
|
102
|
+
]
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
for info in urls.data:
|
|
106
|
+
if info.result == 0 and info.purl:
|
|
107
|
+
print(f"{info.filename}: {cdn}{info.purl}")
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
常用的 `SongFileType`:
|
|
111
|
+
|
|
112
|
+
| 枚举值 | 音质 | 格式 |
|
|
113
|
+
|-----------|-----------|-------------------|
|
|
114
|
+
| `MP3_128` | 标准音质 | MP3 128k |
|
|
115
|
+
| `MP3_320` | HQ 高品质 | MP3 320k |
|
|
116
|
+
| `OGG_192` | HQ 高品质 | OGG 192k |
|
|
117
|
+
| `OGG_320` | HQ 高品质 | OGG 320k |
|
|
118
|
+
| `FLAC` | SQ 无损 | FLAC |
|
|
119
|
+
| `OGG_640` | SQ 无损 | OGG 640k |
|
|
120
|
+
| `MASTER` | 臻品母带 | FLAC 24Bit/192kHz |
|
|
121
|
+
|
|
122
|
+
!!! tip
|
|
123
|
+
|
|
124
|
+
`ACC_96`音质部分 VIP 歌曲即使未登录或无会员,也可能通过该音质获取到播放链接(试听级别)。
|