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