qqmusic-api-python 0.6.1__tar.gz → 0.6.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/PKG-INFO +1 -1
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/release-notes.md +41 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/tutorial/client.md +12 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/__init__.py +1 -1
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/core/client.py +135 -64
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/core/request.py +1 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/core/versioning.py +4 -0
- qqmusic_api_python-0.6.3/qqmusic_api/models/_validator.py +30 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/models/album.py +1 -7
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/models/private_message.py +5 -10
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/models/recommend.py +2 -10
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/models/request.py +11 -3
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/models/singer.py +33 -78
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/models/top.py +7 -10
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/models/user.py +81 -20
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/modules/login.py +3 -1
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/modules/recommend.py +8 -2
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/utils/device.py +4 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/utils/qimei.py +33 -36
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/tests/conftest.py +17 -12
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/tests/test_album.py +6 -6
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/tests/test_user.py +21 -2
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/uv.lock +256 -256
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/run.py +23 -10
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/src/app.py +19 -10
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/src/core/auth.py +30 -30
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/src/core/cache.py +14 -14
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/src/core/config.py +7 -4
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/src/core/credential_store.py +24 -21
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/src/core/security.py +2 -0
- qqmusic_api_python-0.6.3/web/src/modules/comment.py +24 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/src/modules/login.py +28 -5
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/src/modules/mv.py +1 -1
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/src/modules/singer.py +1 -1
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/src/modules/song.py +15 -4
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/src/modules/songlist.py +1 -1
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/src/routes/_helpers.py +9 -4
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/src/routes/comment.py +33 -3
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/src/routes/recommend.py +8 -2
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/src/routing/params.py +27 -3
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/src/routing/route_types.py +1 -1
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/src/routing/router_factory.py +2 -1
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/.agents/skills/pydantic/SKILL.md +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/.agents/skills/python-standards/SKILL.md +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/.agents/skills/tarsio/SKILL.md +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/.agents/skills/tarsio/references/api-reference.md +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/.agents/skills/uv-package-manager/SKILL.md +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/.dockerignore +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/.github/ISSUE_TEMPLATE/bug.yml +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/.github/ISSUE_TEMPLATE/feature.yml +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/.github/renovate.json +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/.github/workflows/checking.yaml +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/.github/workflows/docs.yml +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/.github/workflows/release.yml +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/.github/workflows/testing.yml +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/.gitignore +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/.markdownlint-cli2.yaml +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/AGENTS.md +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/CLAUDE.md +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/LICENSE +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/README.md +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/assets/qq-music.svg +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/cliff.toml +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/coding.md +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/contributing.md +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/index.md +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/reference/core/client.md +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/reference/core/exception.md +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/reference/core/pagination.md +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/reference/core/request.md +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/reference/core/versioning.md +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/reference/model/album.md +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/reference/model/base.md +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/reference/model/comment.md +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/reference/model/login.md +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/reference/model/lyric.md +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/reference/model/mv.md +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/reference/model/recommend.md +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/reference/model/request.md +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/reference/model/search.md +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/reference/model/singer.md +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/reference/model/song.md +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/reference/model/songlist.md +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/reference/model/top.md +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/reference/model/user.md +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/reference/modules/album.md +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/reference/modules/comment.md +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/reference/modules/login.md +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/reference/modules/login_utils.md +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/reference/modules/lyric.md +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/reference/modules/mv.md +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/reference/modules/private_message.md +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/reference/modules/recommend.md +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/reference/modules/search.md +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/reference/modules/singer.md +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/reference/modules/song.md +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/reference/modules/songlist.md +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/reference/modules/top.md +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/reference/modules/user.md +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/tutorial/credential.md +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/tutorial/pagination.md +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/tutorial/start.md +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/docs/tutorial/web.md +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/examples/download_song.py +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/examples/phone_login.py +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/examples/private_message.py +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/examples/qrcode_login.py +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/prek.toml +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/pyproject.toml +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/algorithms/__init__.py +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/algorithms/tripledes.py +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/core/__init__.py +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/core/exceptions.py +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/core/pagination.py +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/models/__init__.py +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/models/base.py +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/models/comment.py +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/models/login.py +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/models/lyric.py +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/models/mv.py +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/models/search.py +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/models/song.py +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/models/songlist.py +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/modules/__init__.py +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/modules/_base.py +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/modules/album.py +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/modules/comment.py +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/modules/login_utils.py +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/modules/lyric.py +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/modules/mv.py +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/modules/private_message.py +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/modules/search.py +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/modules/singer.py +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/modules/song.py +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/modules/songlist.py +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/modules/top.py +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/modules/user.py +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/utils/__init__.py +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/utils/common.py +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/qqmusic_api/utils/mqtt.py +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/scripts/ag-1.py +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/tests/test_comment.py +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/tests/test_login.py +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/tests/test_login_utils.py +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/tests/test_lyric.py +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/tests/test_mv.py +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/tests/test_private_message.py +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/tests/test_recommend.py +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/tests/test_search.py +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/tests/test_singer.py +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/tests/test_song.py +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/tests/test_songlist.py +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/tests/test_top.py +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/.gitignore +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/Dockerfile +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/README.md +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/accounts.example.toml +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/config.example.toml +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/docker-compose.yml +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/src/core/__init__.py +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/src/core/deps.py +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/src/core/response.py +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/src/modules/__init__.py +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/src/routes/__init__.py +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/src/routes/album.py +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/src/routes/login.py +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/src/routes/lyric.py +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/src/routes/mv.py +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/src/routes/search.py +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/src/routes/singer.py +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/src/routes/song.py +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/src/routes/songlist.py +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/src/routes/top.py +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/src/routes/user.py +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/src/routing/__init__.py +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/src/routing/docstrings.py +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/src/routing/executor.py +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/tests/test_web_docstrings.py +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/tests/test_web_enums.py +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/tests/test_web_route_validation.py +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/web/tests/test_web_routes.py +0 -0
- {qqmusic_api_python-0.6.1 → qqmusic_api_python-0.6.3}/zensical.toml +0 -0
|
@@ -1,4 +1,45 @@
|
|
|
1
1
|
|
|
2
|
+
## [[0.6.2](https://github.com/L-1124/QQMusicApi/compare/v0.6.1..v0.6.2)] - 2026-06-08
|
|
3
|
+
|
|
4
|
+
### Bug 修复
|
|
5
|
+
|
|
6
|
+
* **(core)** 将 niquests 网络异常转换为`NetworkError`抛出" ([ff659c5](https://github.com/L-1124/QQMusicApi/commit/ff659c57dae8ad73db74cb00c5ba092b28f36466)) by [@L-1124](https://github.com/L-1124)
|
|
7
|
+
* **(core)** 修复 ANDROID 会话初始化的循环依赖和缓存失效问题 ([4f9be43](https://github.com/L-1124/QQMusicApi/commit/4f9be437724497a789265ede40368628858aea47)) by [@L-1124](https://github.com/L-1124)
|
|
8
|
+
* **(recommend)** 更新获取猜你喜欢推荐接口,支持传入 Credential, 添加 uid 字段到版本策略 ([81062d0](https://github.com/L-1124/QQMusicApi/commit/81062d0091883abb6904562a4ce5223c295d2b24)) by [@L-1124](https://github.com/L-1124)
|
|
9
|
+
|
|
10
|
+
### 功能更新
|
|
11
|
+
|
|
12
|
+
* **(core)** 添加 getSession 匿名会话初始化和设备 OpenUDID 持久化 ([9a4532e](https://github.com/L-1124/QQMusicApi/commit/9a4532e922d3805ba60bc31b213efddbc4c18702)) by [@L-1124](https://github.com/L-1124)
|
|
13
|
+
* **(web)** 补充评论增删路由及推荐认证支持 ([08c9137](https://github.com/L-1124/QQMusicApi/commit/08c9137cb8adcc4ce29c0843f25b4a160819839b)) by [@L-1124](https://github.com/L-1124)
|
|
14
|
+
|
|
15
|
+
### 功能重构
|
|
16
|
+
|
|
17
|
+
* **(web)** 重构日志系统 ([20fe8c6](https://github.com/L-1124/QQMusicApi/commit/20fe8c62af0477c5bf1e4afd82f08dcd96881031)) by [@L-1124](https://github.com/L-1124)
|
|
18
|
+
* **(web)** 统一代码风格与日志惰性求值 ([4ba0bf0](https://github.com/L-1124/QQMusicApi/commit/4ba0bf06b0e3a88bda6efea699c60ec068b16e91)) by [@L-1124](https://github.com/L-1124)
|
|
19
|
+
* **(web)** 补齐路由辅助函数与应用入口类型注解 ([080b2fe](https://github.com/L-1124/QQMusicApi/commit/080b2fe99969f7256005c5a2f23793cacfd6db85)) by [@L-1124](https://github.com/L-1124)
|
|
20
|
+
* **(web/routing)** 补齐路由类型与参数校验器类型注解 ([bcedc66](https://github.com/L-1124/QQMusicApi/commit/bcedc66bbb34d768957b36493250e067c4714e74)) by [@L-1124](https://github.com/L-1124)
|
|
21
|
+
|
|
22
|
+
### 贡献者
|
|
23
|
+
|
|
24
|
+
* @L-1124
|
|
25
|
+
* @github-actions[bot]
|
|
26
|
+
|
|
27
|
+
## [[0.6.1](https://github.com/L-1124/QQMusicApi/compare/v0.6.0..v0.6.1)] - 2026-05-20
|
|
28
|
+
|
|
29
|
+
### Bug 修复
|
|
30
|
+
|
|
31
|
+
* 传入的platform不生效 ([b19bec5](https://github.com/L-1124/QQMusicApi/commit/b19bec52f044d6508d9506f7111fea6eee5b42de)) by [@L-1124](https://github.com/L-1124)
|
|
32
|
+
|
|
33
|
+
### 功能更新
|
|
34
|
+
|
|
35
|
+
* **(comment)** 支持添加评论和删除评论功能 ([dc5f568](https://github.com/L-1124/QQMusicApi/commit/dc5f5685d31f48b5dd8fc6bfcfdb6fb357884e7e)) by [@L-1124](https://github.com/L-1124)
|
|
36
|
+
* **(private-message)** 新增私信接口模块 ([0ce6c77](https://github.com/L-1124/QQMusicApi/commit/0ce6c77b423c01c684cf86ff7a7ee9ca9a70d9fa)) by [@L-1124](https://github.com/L-1124)
|
|
37
|
+
|
|
38
|
+
### 贡献者
|
|
39
|
+
|
|
40
|
+
* @L-1124
|
|
41
|
+
* @github-actions[bot]
|
|
42
|
+
|
|
2
43
|
## [[0.6.0](https://github.com/L-1124/QQMusicApi/compare/v0.5.3..v0.6.0)] - 2026-05-09
|
|
3
44
|
|
|
4
45
|
### Bug 修复
|
|
@@ -94,3 +94,15 @@ async def main():
|
|
|
94
94
|
asyncio.run(_main())
|
|
95
95
|
|
|
96
96
|
```
|
|
97
|
+
|
|
98
|
+
## 设备信息
|
|
99
|
+
|
|
100
|
+
可通过 `device_path` 参数指定设备信息文件的路径进行持久化存储:
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
client = Client(device_path="device.json")
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
不传 `device_path` 则仅在内存维护设备状态,重启后丢失。
|
|
107
|
+
|
|
108
|
+
`Client.credential` 更改时设备信息保持不变。
|
|
@@ -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
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
-
|
|
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=
|
|
355
|
+
platform=target_platform,
|
|
291
356
|
credential=credential or self.credential,
|
|
292
|
-
device=
|
|
357
|
+
device=device,
|
|
293
358
|
qimei=cast("dict[str, str]", await self._qimei_manager.get_cached())
|
|
294
|
-
if
|
|
359
|
+
if target_platform == Platform.ANDROID
|
|
295
360
|
else None,
|
|
296
|
-
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(
|
|
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
|
-
"
|
|
320
|
-
|
|
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
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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
|
-
|
|
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
|
|
|
@@ -88,6 +88,8 @@ class VersionPolicy:
|
|
|
88
88
|
device.model,
|
|
89
89
|
device.version.sdk,
|
|
90
90
|
device.fingerprint,
|
|
91
|
+
device.session_uid,
|
|
92
|
+
device.session_sid,
|
|
91
93
|
)
|
|
92
94
|
if platform == Platform.ANDROID
|
|
93
95
|
else (),
|
|
@@ -113,7 +115,9 @@ class VersionPolicy:
|
|
|
113
115
|
QIMEI36=qimei["q36"] if qimei is not None else "",
|
|
114
116
|
OpenUDID=guid,
|
|
115
117
|
udid=guid,
|
|
118
|
+
uid=device.session_uid,
|
|
116
119
|
OpenUDID2=guid,
|
|
120
|
+
sid=device.session_sid,
|
|
117
121
|
aid=device.android_id,
|
|
118
122
|
os_ver=device.version.release,
|
|
119
123
|
phonetype=device.model,
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Pydantic Validator."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from pydantic import BeforeValidator
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _none_to_empty_list(value: list[Any] | None) -> list[Any]:
|
|
9
|
+
"""将 ``None`` 规整为空列表."""
|
|
10
|
+
return [] if value is None else value
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _none_to_empty_dict(value: dict[str, Any] | None) -> dict[str, Any]:
|
|
14
|
+
"""将 ``None`` 规整为空字典."""
|
|
15
|
+
return {} if value is None else value
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _none_or_zero_to_empty_str(value: str | int | None) -> str:
|
|
19
|
+
"""将 ``None`` 或 ``0`` 规整为空字符串."""
|
|
20
|
+
return "" if value in (None, 0) else str(value)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
#: 将 ``None`` 规整为空列表的 BeforeValidator.
|
|
24
|
+
NoneToEmptyList = BeforeValidator(_none_to_empty_list)
|
|
25
|
+
|
|
26
|
+
#: 将 ``None`` 规整为空字典的 BeforeValidator.
|
|
27
|
+
NoneToEmptyDict = BeforeValidator(_none_to_empty_dict)
|
|
28
|
+
|
|
29
|
+
#: 将 ``None`` 或 ``0`` 规整为空字符串的 BeforeValidator.
|
|
30
|
+
NoneOrZeroToEmptyStr = BeforeValidator(_none_or_zero_to_empty_str)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Album API 返回模型定义."""
|
|
2
2
|
|
|
3
|
-
from pydantic import Field
|
|
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
|
"""私信发送响应.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
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 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="
|
|
73
|
-
|
|
74
|
-
@model_validator(mode="before")
|
|
75
|
-
@classmethod
|
|
76
|
-
def _normalize_tracks(cls, data: Any) -> Any:
|
|
77
|
-
"""将猜你喜欢响应规整为稳定的歌曲列表载荷."""
|
|
78
|
-
if isinstance(data, dict) and "Tracks" not in data:
|
|
79
|
-
return {"Tracks": []}
|
|
80
|
-
return data
|
|
72
|
+
songs: list[Song] = Field(default_factory=list, alias="tracks")
|
|
81
73
|
|
|
82
74
|
|
|
83
75
|
class RadarRecommendResponse(Response):
|
|
@@ -45,6 +45,8 @@ class CommonParams(BaseModel):
|
|
|
45
45
|
udid: str | None = Field(default=None)
|
|
46
46
|
aid: str | None = Field(default=None)
|
|
47
47
|
guid: str | None = Field(default=None)
|
|
48
|
+
uid: str | None = Field(default=None)
|
|
49
|
+
sid: str | None = Field(default=None)
|
|
48
50
|
os_ver: str | None = Field(default=None)
|
|
49
51
|
phonetype: str | None = Field(default=None)
|
|
50
52
|
devicelevel: str | None = Field(default=None)
|
|
@@ -102,6 +104,7 @@ class Credential(BaseModel):
|
|
|
102
104
|
if "loginType" in data or "login_type" in data:
|
|
103
105
|
return data
|
|
104
106
|
|
|
107
|
+
# TODO: 修复没有 musickey 时误判为 QQ 登录的问题
|
|
105
108
|
musickey = data.get("musickey", "")
|
|
106
109
|
inferred_login_type = 1 if isinstance(musickey, str) and musickey.startswith("W_X") else 2
|
|
107
110
|
return {**data, "loginType": inferred_login_type}
|
|
@@ -176,9 +179,14 @@ class Response(BaseModel):
|
|
|
176
179
|
matches = jsonpath_expr.find(data)
|
|
177
180
|
|
|
178
181
|
if matches:
|
|
179
|
-
|
|
180
|
-
|
|
182
|
+
values = [match.value for match in matches]
|
|
183
|
+
if "[*]" in jsonpath_expr_str:
|
|
184
|
+
# 通配符路径表示提取条目列表, 即使仅命中一条也保持列表,
|
|
185
|
+
# 兼容上游在仅有一条数据时直接返回对象而非数组的行为.
|
|
186
|
+
processed_data[target_key] = values
|
|
187
|
+
elif len(values) == 1:
|
|
188
|
+
processed_data[target_key] = values[0]
|
|
181
189
|
else:
|
|
182
|
-
processed_data[target_key] =
|
|
190
|
+
processed_data[target_key] = values
|
|
183
191
|
|
|
184
192
|
return processed_data
|