├── logo.png ├── .gitignore ├── tsugu_api_core ├── __init__.py ├── client │ ├── __init__.py │ ├── _client.py │ ├── aiohttp.py │ └── httpx.py ├── _settings.py ├── utils.py ├── exception.py ├── _typing.py └── _network.py ├── setup.py ├── LICENSE ├── .github └── workflows │ └── publish-to-pypi.yml ├── tsugu_api ├── _bandoristation.py ├── __init__.py ├── _station.py ├── _user.py └── _tsugu.py ├── tsugu_api_async ├── _bandoristation.py ├── __init__.py ├── _station.py ├── _user.py └── _tsugu.py └── README.md /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WindowsSov8forUs/tsugu-api-python/HEAD/logo.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/__pycache__/ 2 | /.vscode/ 3 | test.py 4 | /tsugu_api_python.egg-info/ 5 | /build/ 6 | /dist/ 7 | /.venv/ 8 | requirements.txt -------------------------------------------------------------------------------- /tsugu_api_core/__init__.py: -------------------------------------------------------------------------------- 1 | '''`tsugu_api_core` tsugu-api-python 核心模块''' 2 | 3 | from . import utils as utils 4 | from . import client as client 5 | from . import _typing as _typing 6 | from . import _network as _network 7 | from . import exception as exception 8 | 9 | from .client import register_client as register_client 10 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages, setup 2 | 3 | with open('README.md', 'r', encoding='utf-8') as readme: 4 | long_description = readme.read() 5 | 6 | setup( 7 | name='tsugu-api-python', 8 | version='1.5.10', 9 | author='WindowsSov8', 10 | author_email='qwertyuiop2333@hotmail.com', 11 | description='Tsugu BanGDream Bot 的功能 API 统合包', 12 | long_description=long_description, 13 | long_description_content_type='text/markdown', 14 | url='https://github.com/WindowsSov8forUs/tsugu-api-python', 15 | include_package_data=False, 16 | packages=find_packages(), 17 | license='MIT', 18 | classifiers=[ 19 | 'Programming Language :: Python :: 3.8', 20 | 'License :: OSI Approved :: MIT License', 21 | 'Operating System :: OS Independent' 22 | ], 23 | python_requires='>=3.8', 24 | install_requires=[ 25 | 'typing_extensions>=4.5.0', 26 | ], 27 | extras_require={ 28 | 'httpx': [ 29 | 'httpx>=0.22.0', 30 | ], 31 | 'aiohttp': [ 32 | 'aiohttp>=3.8.1', 33 | ], 34 | }, 35 | ) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 WindowsSov8 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /tsugu_api_core/client/__init__.py: -------------------------------------------------------------------------------- 1 | '''`tsugu_api_python` HTTP 客户端模块''' 2 | 3 | from typing import Type, Optional 4 | 5 | from tsugu_api_core._settings import settings 6 | 7 | from ._client import Client as Client 8 | from ._client import Request as Request 9 | from ._client import Response as Response 10 | 11 | __HTTPClient__: Optional[Type[Client]] = None 12 | 13 | def register_client( 14 | client: Type[Client] 15 | ) -> None: 16 | '''注册一个 HTTP 客户端类,之后将会使用该客户端发送请求而不是内置客户端。 17 | 18 | 参数: 19 | client (Type[Client]): HTTP 客户端类 20 | ''' 21 | global __HTTPClient__ 22 | __HTTPClient__ = client 23 | return 24 | 25 | def _Client() -> Type[Client]: 26 | '''获取当前使用的 HTTP 客户端 27 | 28 | 返回: 29 | Client: HTTP 客户端 30 | ''' 31 | global __HTTPClient__ 32 | if __HTTPClient__ is not None: 33 | return __HTTPClient__ 34 | 35 | if settings.client.lower() == 'httpx': 36 | from .httpx import Client as HTTPXClient 37 | __HTTPClient__ = HTTPXClient 38 | return __HTTPClient__ 39 | elif settings.client.lower() == 'aiohttp': 40 | from .aiohttp import Client as AIOHTTPClient 41 | __HTTPClient__ = AIOHTTPClient 42 | return __HTTPClient__ 43 | else: 44 | raise ValueError('Unknown client type') -------------------------------------------------------------------------------- /.github/workflows/publish-to-pypi.yml: -------------------------------------------------------------------------------- 1 | name: Upload Python Package 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 7 | 8 | workflow_dispatch: 9 | 10 | jobs: 11 | deploy: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v3 17 | 18 | - name: Set up Python 19 | uses: actions/setup-python@v4 20 | with: 21 | python-version: '3.10' 22 | 23 | - name: Install dependencies 24 | run: | 25 | python -m pip install --upgrade pip 26 | pip install build twine 27 | 28 | - name: Get Version 29 | id: version 30 | run: | 31 | echo "VERSION=$(pip --version)" >> $GITHUB_OUTPUT 32 | echo "TAG_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT 33 | echo "TAG_NAME=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT 34 | 35 | - name: Build distribution 36 | run: | 37 | python -m build 38 | 39 | - name: Publish to PyPI and Release 40 | run: | 41 | export GH_TOKEN=$GITHUB_TOKEN 42 | gh release upload --clobber ${{ steps.version.outputs.TAG_NAME }} dist/*.tar.gz dist/*.whl 43 | python -m twine upload dist/* --repository-url https://upload.pypi.org/legacy/ --username __token__ --password ${{ secrets.PYPI_TOKEN }} 44 | env: 45 | GITHUB_TOKEN: ${{ secrets.PAT }} -------------------------------------------------------------------------------- /tsugu_api_core/_settings.py: -------------------------------------------------------------------------------- 1 | '''对 tsugu_api 进行配置''' 2 | 3 | from enum import Enum 4 | 5 | class Settings: 6 | '''tsugu_api 配置类''' 7 | 8 | client: str = 'httpx' 9 | '''使用的 HTTP 客户端''' 10 | 11 | timeout: float = 10 12 | '''请求超时时间''' 13 | 14 | max_retries: int = 3 15 | '''请求重试次数''' 16 | 17 | proxy: str = '' 18 | '''代理地址''' 19 | 20 | backend_url: str = 'http://tsugubot.com:8080' 21 | ''' 22 | 后端地址 23 | 24 | 默认为 Tsugu 官方后端,若有自建后端服务器可进行修改。 25 | ''' 26 | backend_proxy: bool = True 27 | ''' 28 | 是否使用后端代理 29 | 30 | 当设置代理地址后可修改此项以决定是否使用代理。 31 | 32 | 默认为 True,即使用后端代理。若使用代理时后端服务器无法访问,可将此项设置为 False。 33 | ''' 34 | userdata_backend_url: str = 'http://tsugubot.com:8080' 35 | ''' 36 | 用户数据后端地址 37 | 38 | 所有的 `/user` 路由和 `/station` 路由都基于此 39 | 40 | 由于这些路由需要用户数据库的支持,部分 tsugu 后端可能不存在该路由 41 | 42 | 默认为 Tsugu 官方后端,若有自建后端服务器可进行修改。 43 | ''' 44 | userdata_backend_proxy: bool = True 45 | ''' 46 | 是否使用用户数据后端代理 47 | 48 | 当设置代理地址后可修改此项以决定是否使用代理。 49 | 50 | 默认为 True,即使用后端代理。若使用代理时后端服务器无法访问,可将此项设置为 False。 51 | ''' 52 | 53 | use_easy_bg: bool = True 54 | ''' 55 | 是否使用简易背景,使用可在降低背景质量的前提下加快响应速度。 56 | 57 | 默认为 True,即使用简易背景。若不使用简易背景,可将此项设置为 False。 58 | ''' 59 | compress: bool = True 60 | ''' 61 | 是否压缩返回数据,压缩可减少返回数据大小。 62 | 63 | 默认为 True,即压缩返回数据。若不压缩返回数据,可将此项设置为 False。 64 | ''' 65 | 66 | settings = Settings() 67 | 68 | __all__ = [ 69 | 'settings', 70 | ] 71 | -------------------------------------------------------------------------------- /tsugu_api/_bandoristation.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from tsugu_api_core._settings import settings 4 | from tsugu_api_core._typing import StationRoom 5 | from tsugu_api_core._network import BANDORI_STATION_URL, Api 6 | from tsugu_api_core.exception import ( 7 | RoomQueryFailure, 8 | RoomSubmitFailure 9 | ) 10 | 11 | def submit_room_number(number: int, user_id: str, raw_message: str, source: str, token: str) -> None: 12 | '''上传房间号 13 | 14 | 参数: 15 | number (int): 房间号 16 | user_id (str): 用户 ID 17 | raw_message (str): 原始消息,用作房间号注释 18 | source (str): 房间来源,即令牌名称 19 | token (str): 上传用的车站令牌 20 | ''' 21 | 22 | # 构建参数 23 | params = { 24 | 'number': number, 25 | 'user_id': user_id, 26 | 'raw_message': raw_message, 27 | 'source': source, 28 | 'token': token, 29 | 'function': 'submit_room_number', 30 | } 31 | 32 | # 发送请求 33 | response = Api( 34 | BANDORI_STATION_URL, 35 | '', 36 | proxy=settings.backend_proxy 37 | ).get(params).json() 38 | if response['status'] == 'failure': 39 | raise RoomSubmitFailure(response['response']) 40 | 41 | def query_room_number() -> List[StationRoom]: 42 | '''获取房间号 43 | 44 | 返回: 45 | List[StationRoom]: 房间信息列表 46 | ''' 47 | 48 | # 发送请求 49 | response = Api( 50 | BANDORI_STATION_URL, 51 | '', 52 | proxy=settings.backend_proxy 53 | ).get({'function': 'query_room_number'}).json() 54 | if response['status'] == 'failure': 55 | raise RoomQueryFailure(response['response']) 56 | return response['response'] 57 | -------------------------------------------------------------------------------- /tsugu_api_async/_bandoristation.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from tsugu_api_core._settings import settings 4 | from tsugu_api_core._typing import StationRoom 5 | from tsugu_api_core._network import BANDORI_STATION_URL, Api 6 | from tsugu_api_core.exception import ( 7 | RoomQueryFailure, 8 | RoomSubmitFailure 9 | ) 10 | 11 | async def submit_room_number(number: int, user_id: str, raw_message: str, source: str, token: str) -> None: 12 | '''上传房间号 13 | 14 | 参数: 15 | number (int): 房间号 16 | user_id (str): 用户 ID 17 | raw_message (str): 原始消息,用作房间号注释 18 | source (str): 房间来源,即令牌名称 19 | token (str): 上传用的车站令牌 20 | ''' 21 | 22 | # 构建参数 23 | params = { 24 | 'number': number, 25 | 'user_id': user_id, 26 | 'raw_message': raw_message, 27 | 'source': source, 28 | 'token': token, 29 | 'function': 'submit_room_number' 30 | } 31 | 32 | # 发送请求 33 | response = (await Api( 34 | BANDORI_STATION_URL, 35 | '', 36 | proxy=settings.backend_proxy 37 | ).aget(params)).json() 38 | if response['status'] == 'failure': 39 | raise RoomSubmitFailure(response['response']) 40 | 41 | async def query_room_number() -> List[StationRoom]: 42 | '''获取房间号 43 | 44 | 返回: 45 | List[StationRoom]: 房间信息列表 46 | ''' 47 | 48 | # 发送请求 49 | response = (await Api( 50 | BANDORI_STATION_URL, 51 | '', 52 | proxy=settings.backend_proxy 53 | ).aget({'function': 'query_room_number'})).json() 54 | if response['status'] == 'failure': 55 | raise RoomQueryFailure(response['response']) 56 | return response['response'] 57 | -------------------------------------------------------------------------------- /tsugu_api/__init__.py: -------------------------------------------------------------------------------- 1 | ''' 2 | `tsugu_api` 向 Tsugu 后端 API 请求模块 3 | ''' 4 | 5 | from tsugu_api_core.utils import * 6 | 7 | from ._user import get_user_data as get_user_data 8 | from ._user import change_user_data as change_user_data 9 | from ._user import bind_player_request as bind_player_request 10 | from ._user import bind_player_verification as bind_player_verification 11 | 12 | from ._tsugu import ycx as ycx 13 | from ._tsugu import lsycx as lsycx 14 | from ._tsugu import ycx_all as ycx_all 15 | from ._tsugu import room_list as room_list 16 | from ._tsugu import song_meta as song_meta 17 | from ._tsugu import cutoff_all as cutoff_all 18 | from ._tsugu import song_chart as song_chart 19 | from ._tsugu import event_stage as event_stage 20 | from ._tsugu import search_card as search_card 21 | from ._tsugu import search_song as search_song 22 | from ._tsugu import song_random as song_random 23 | from ._tsugu import fuzzy_search as fuzzy_search 24 | from ._tsugu import search_event as search_event 25 | from ._tsugu import search_gacha as search_gacha 26 | from ._tsugu import cutoff_detail as cutoff_detail 27 | from ._tsugu import search_player as search_player 28 | from ._tsugu import gacha_simulate as gacha_simulate 29 | from ._tsugu import search_character as search_character 30 | from ._tsugu import get_card_illustration as get_card_illustration 31 | from ._tsugu import cutoff_list_of_recent_event as cutoff_list_of_recent_event 32 | 33 | from ._station import station_query_all_room as station_query_all_room 34 | from ._station import station_submit_room_number as station_submit_room_number 35 | 36 | from ._bandoristation import query_room_number as query_room_number 37 | from ._bandoristation import submit_room_number as submit_room_number 38 | 39 | from tsugu_api_core._settings import settings as settings 40 | -------------------------------------------------------------------------------- /tsugu_api_async/__init__.py: -------------------------------------------------------------------------------- 1 | ''' 2 | `tsugu_api` 向 Tsugu 后端 API 请求模块 3 | ''' 4 | 5 | from tsugu_api_core.utils import * 6 | 7 | from ._user import get_user_data as get_user_data 8 | from ._user import change_user_data as change_user_data 9 | from ._user import bind_player_request as bind_player_request 10 | from ._user import bind_player_verification as bind_player_verification 11 | 12 | from ._tsugu import ycx as ycx 13 | from ._tsugu import lsycx as lsycx 14 | from ._tsugu import ycx_all as ycx_all 15 | from ._tsugu import room_list as room_list 16 | from ._tsugu import song_meta as song_meta 17 | from ._tsugu import cutoff_all as cutoff_all 18 | from ._tsugu import song_chart as song_chart 19 | from ._tsugu import event_stage as event_stage 20 | from ._tsugu import search_card as search_card 21 | from ._tsugu import search_song as search_song 22 | from ._tsugu import song_random as song_random 23 | from ._tsugu import fuzzy_search as fuzzy_search 24 | from ._tsugu import search_event as search_event 25 | from ._tsugu import search_gacha as search_gacha 26 | from ._tsugu import cutoff_detail as cutoff_detail 27 | from ._tsugu import search_player as search_player 28 | from ._tsugu import gacha_simulate as gacha_simulate 29 | from ._tsugu import search_character as search_character 30 | from ._tsugu import get_card_illustration as get_card_illustration 31 | from ._tsugu import cutoff_list_of_recent_event as cutoff_list_of_recent_event 32 | 33 | from ._station import station_query_all_room as station_query_all_room 34 | from ._station import station_submit_room_number as station_submit_room_number 35 | 36 | from ._bandoristation import query_room_number as query_room_number 37 | from ._bandoristation import submit_room_number as submit_room_number 38 | 39 | from tsugu_api_core._settings import settings as settings 40 | -------------------------------------------------------------------------------- /tsugu_api/_station.py: -------------------------------------------------------------------------------- 1 | from time import time 2 | from typing import Optional 3 | 4 | from tsugu_api_core._network import Api 5 | from tsugu_api_core._settings import settings 6 | from tsugu_api_core._typing import ( 7 | _QueryResponse, 8 | _SubmitResponse 9 | ) 10 | 11 | def station_submit_room_number( 12 | number: int, 13 | raw_message: str, 14 | platform: str, 15 | user_id: str, 16 | user_name: str, 17 | avatar_url: Optional[str] = None, 18 | bandori_station_token: Optional[str] = None 19 | ) -> _SubmitResponse: 20 | '''提交车牌号 21 | 22 | 参数: 23 | number (int): 车牌号 24 | raw_message (str): 原始消息 25 | platform (str): 平台 26 | user_id (str): 用户 ID 27 | user_name (str): 用户名 28 | avatar_url (Optional[str]): 用户头像 URL 29 | bandori_station_token (Optional[str]): Bandori 车站令牌,不填则使用 Tsugu 后端配置 30 | 31 | 返回: 32 | _SubmitResponse: 响应信息 33 | ''' 34 | 35 | # 构建数据 36 | data = { 37 | 'number': number, 38 | 'rawMessage': raw_message, 39 | 'platform': platform, 40 | 'userId': user_id, 41 | 'userName': user_name, 42 | 'time': int(time()) 43 | } 44 | if avatar_url: 45 | data['avatarUrl'] = avatar_url 46 | if bandori_station_token: 47 | data['bandoriStationToken'] = bandori_station_token 48 | 49 | # 发送请求 50 | return Api( 51 | settings.userdata_backend_url, 52 | '/station/submitRoomNumber', 53 | proxy=settings.userdata_backend_proxy 54 | ).post(data).json() 55 | 56 | def station_query_all_room() -> _QueryResponse: 57 | '''查询最近车站车牌 58 | 59 | 返回: 60 | _QueryResponse: 响应信息 61 | ''' 62 | 63 | # 发送请求 64 | return Api( 65 | settings.userdata_backend_url, 66 | '/station/queryAllRoom', 67 | proxy=settings.userdata_backend_proxy 68 | ).get().json() 69 | -------------------------------------------------------------------------------- /tsugu_api_async/_station.py: -------------------------------------------------------------------------------- 1 | from time import time 2 | from typing import Optional 3 | 4 | from tsugu_api_core._network import Api 5 | from tsugu_api_core._settings import settings 6 | from tsugu_api_core._typing import ( 7 | _QueryResponse, 8 | _SubmitResponse 9 | ) 10 | 11 | async def station_submit_room_number( 12 | number: int, 13 | raw_message: str, 14 | platform: str, 15 | user_id: str, 16 | user_name: str, 17 | avatar_url: Optional[str] = None, 18 | bandori_station_token: Optional[str] = None 19 | ) -> _SubmitResponse: 20 | '''提交车牌号 21 | 22 | 参数: 23 | number (int): 车牌号 24 | raw_message (str): 原始消息 25 | platform (str): 平台 26 | user_id (str): 用户 ID 27 | user_name (str): 用户名 28 | avatar_url (Optional[str]): 用户头像 URL 29 | bandori_station_token (Optional[str]): Bandori 车站令牌,不填则使用 Tsugu 后端配置 30 | 31 | 返回: 32 | _SubmitResponse: 响应信息 33 | ''' 34 | 35 | # 构建数据 36 | data = { 37 | 'number': number, 38 | 'rawMessage': raw_message, 39 | 'platform': platform, 40 | 'userId': user_id, 41 | 'userName': user_name, 42 | 'time': int(time()) 43 | } 44 | if avatar_url: 45 | data['avatarUrl'] = avatar_url 46 | if bandori_station_token: 47 | data['bandoriStationToken'] = bandori_station_token 48 | 49 | # 发送请求 50 | return (await Api( 51 | settings.userdata_backend_url, 52 | '/station/submitRoomNumber', 53 | proxy=settings.userdata_backend_proxy 54 | ).apost(data)).json() 55 | 56 | async def station_query_all_room() -> _QueryResponse: 57 | '''查询最近车站车牌 58 | 59 | 返回: 60 | _QueryResponse: 响应信息 61 | ''' 62 | 63 | # 发送请求 64 | return (await Api( 65 | settings.userdata_backend_url, 66 | '/station/queryAllRoom', 67 | proxy=settings.userdata_backend_proxy 68 | ).aget()).json() 69 | -------------------------------------------------------------------------------- /tsugu_api_core/utils.py: -------------------------------------------------------------------------------- 1 | ''' 2 | `tsugu_api_core.utils` Tsugu API 工具模块 3 | ''' 4 | 5 | from tsugu_api_core._typing import ( 6 | _Room, 7 | ServerId, 8 | ServerName, 9 | StationRoom, 10 | ) 11 | 12 | def string_to_server_code(server: str) -> ServerId: 13 | if server == 'jp': 14 | return 0 15 | elif server == 'en': 16 | return 1 17 | elif server == 'tw': 18 | return 2 19 | elif server == 'cn': 20 | return 3 21 | elif server == 'kr': 22 | return 4 23 | elif server == '日服': 24 | return 0 25 | elif server == '国际服': 26 | return 1 27 | elif server == '台服': 28 | return 2 29 | elif server == '国服': 30 | return 3 31 | elif server == '韩服': 32 | return 4 33 | else: 34 | raise ValueError('服务器名称不存在') 35 | 36 | def int_to_server_short(server: int) -> ServerName: 37 | if server == 0: 38 | return 'jp' 39 | elif server == 1: 40 | return 'en' 41 | elif server == 2: 42 | return 'tw' 43 | elif server == 3: 44 | return 'cn' 45 | elif server == 4: 46 | return 'kr' 47 | else: 48 | raise ValueError('服务器代码不存在') 49 | 50 | def int_to_server_full(server: int) -> str: 51 | if server == 0: 52 | return '日服' 53 | elif server == 1: 54 | return '国际服' 55 | elif server == 2: 56 | return '台服' 57 | elif server == 3: 58 | return '国服' 59 | elif server == 4: 60 | return '韩服' 61 | else: 62 | raise ValueError('服务器代码不存在') 63 | 64 | def station_room_to_tsugu(station_room: StationRoom) -> _Room: 65 | room: _Room = { 66 | 'number': station_room['number'], 67 | 'rawMessage': station_room['raw_message'], 68 | 'source': station_room['source_info']['name'], 69 | 'userId': str(station_room['user_info']['user_id']), 70 | 'time': station_room['time'], 71 | 'avatarUrl': station_room['user_info']['avatar'], 72 | 'userName': station_room['user_info']['username'], 73 | } 74 | return room 75 | 76 | __all__ = [ 77 | 'string_to_server_code', 78 | 'int_to_server_short', 79 | 'int_to_server_full', 80 | 'station_room_to_tsugu', 81 | ] 82 | -------------------------------------------------------------------------------- /tsugu_api_core/client/_client.py: -------------------------------------------------------------------------------- 1 | '''HTTP 客户端类型基类''' 2 | 3 | from json import loads 4 | from abc import ABC, abstractmethod 5 | from typing import Any, Dict, Optional 6 | 7 | from typing_extensions import Self 8 | 9 | class Request: 10 | '''HTTP 请求类''' 11 | 12 | def __init__( 13 | self, 14 | method: str, 15 | url: str, 16 | *, 17 | headers: Optional[Dict[str, str]], 18 | params: Optional[Dict[str, Any]], 19 | data: Optional[Dict[str, Any]], 20 | ) -> None: 21 | self.method = method 22 | '''请求方法''' 23 | self.url = url 24 | '''请求 URL''' 25 | self.headers = headers 26 | '''请求头''' 27 | self.params = params 28 | '''请求参数''' 29 | self.data = data 30 | '''请求数据''' 31 | 32 | class Response: 33 | '''HTTP 响应类''' 34 | 35 | def __init__(self, content: bytes, status_code: int, exception: Optional[Exception]=None) -> None: 36 | self.content = content 37 | '''响应内容''' 38 | self.status_code = status_code 39 | '''状态码''' 40 | self.exception = exception 41 | '''异常''' 42 | 43 | def json(self, **kwargs: Any) -> Any: 44 | '''解析 JSON 响应内容''' 45 | return loads(self.content, **kwargs) 46 | 47 | class Client(ABC): 48 | '''HTTP 客户端类型基类''' 49 | 50 | def __init__(self, proxy: Optional[str], timeout: float, max_retries: int) -> None: 51 | self.proxy = proxy 52 | '''代理服务器地址''' 53 | self.timeout = timeout 54 | '''超时时间''' 55 | self.max_retries = max_retries 56 | '''重试次数''' 57 | 58 | @abstractmethod 59 | def __enter__(self) -> Self: 60 | raise NotImplementedError 61 | 62 | @abstractmethod 63 | async def __aenter__(self) -> Self: 64 | raise NotImplementedError 65 | 66 | @abstractmethod 67 | def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None: 68 | raise NotImplementedError 69 | 70 | @abstractmethod 71 | async def __aexit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None: 72 | raise NotImplementedError 73 | 74 | @abstractmethod 75 | def request(self, request: Request) -> Response: 76 | '''发送请求并获取响应''' 77 | raise NotImplementedError 78 | 79 | @abstractmethod 80 | async def arequest(self, request: Request) -> Response: 81 | '''异步发送请求并获取响应''' 82 | raise NotImplementedError -------------------------------------------------------------------------------- /tsugu_api_core/client/aiohttp.py: -------------------------------------------------------------------------------- 1 | '''内置的 AIOHTTP 适配''' 2 | 3 | import asyncio 4 | from json import dumps 5 | from typing import Any, cast 6 | from typing_extensions import override 7 | 8 | from ._client import Client as _Client 9 | from ._client import Request, Response 10 | 11 | try: 12 | import aiohttp 13 | except ModuleNotFoundError as exception: 14 | raise ImportError( 15 | 'module \'aiohttp\' is not installed, please install it by running \'pip install aiohttp\'' 16 | ) from exception 17 | 18 | class Client(_Client): 19 | _client_session: aiohttp.ClientSession 20 | 21 | @override 22 | def __enter__(self) -> 'Client': 23 | raise RuntimeError('AIOHTTP client is not synchronous, please use async context manager') 24 | 25 | @override 26 | async def __aenter__(self) -> 'Client': 27 | self._client_session = aiohttp.ClientSession( 28 | trust_env=False, 29 | timeout=aiohttp.ClientTimeout(total=self.timeout), 30 | proxy=self.proxy, 31 | ) 32 | await self._client_session.__aenter__() 33 | return self 34 | 35 | @override 36 | def __exit__(self, *args: Any) -> None: 37 | pass 38 | 39 | @override 40 | async def __aexit__(self, *args: Any) -> None: 41 | await self._client_session.__aexit__(*args) 42 | 43 | @override 44 | def request(self, request: Request) -> Response: 45 | raise RuntimeError('AIOHTTP client is not synchronous, please use async request method.') 46 | 47 | @override 48 | async def arequest(self, request: Request) -> Response: 49 | retries = 0 50 | 51 | while True: 52 | try: 53 | response = await self._client_session.request( 54 | request.method, 55 | request.url, 56 | params=request.params, 57 | data=cast(dict, dumps(request.data)) if request.data is not None else request.data, 58 | headers=request.headers, 59 | ) 60 | break 61 | except Exception as exception: 62 | if retries >= self.max_retries: 63 | raise exception 64 | retries += 1 65 | await asyncio.sleep(0.5) 66 | 67 | try: 68 | response.raise_for_status() 69 | return Response( 70 | await response.read(), 71 | response.status, 72 | ) 73 | except Exception as exception: 74 | return Response( 75 | await response.read(), 76 | response.status, 77 | exception, 78 | ) -------------------------------------------------------------------------------- /tsugu_api_core/exception.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from tsugu_api_core._typing import _FailedResponse, _BadRequestResponse 3 | 4 | class TsuguException(Exception): 5 | '''tsugu_api_core 异常基类''' 6 | msg: str = '' 7 | '''从 API 返回参数中获取的错误信息''' 8 | def __init__(self, msg: str) -> None: 9 | '''初始化''' 10 | self.msg = msg 11 | 12 | def __str__(self) -> str: 13 | return self.msg 14 | 15 | class HTTPStatusError(TsuguException): 16 | '''HTTP 状态码错误''' 17 | status_code: int 18 | '''HTTP 状态码''' 19 | def __init__(self, status_code: int) -> None: 20 | '''初始化''' 21 | self.status_code = status_code 22 | super().__init__(f"HTTP status code error: {status_code}, please check your network environment or contact the developer.") 23 | return 24 | 25 | class BadRequestError(TsuguException): 26 | '''请求的参数错误''' 27 | api: str 28 | '''请求的 API''' 29 | response: _BadRequestResponse 30 | '''API 返回的响应''' 31 | def __init__(self, api: str, response: _BadRequestResponse) -> None: 32 | '''初始化''' 33 | self.api = api 34 | self.response = response 35 | 36 | error_messages: List[str] = [] 37 | for error in response['error']: 38 | error_messages.append( 39 | f"\tGot a wrong value '{str(error['value'])}' for {error['type']} parameter '{error['path']}' in {error['location']}: {error['msg']}." 40 | ) 41 | 42 | super().__init__(f"API '{api}' got wrong parameters:\n" + '\n'.join(error_messages)) 43 | 44 | return 45 | 46 | class FailedException(TsuguException): 47 | '''API 请求失败''' 48 | api: str 49 | '''请求的 API''' 50 | status_code: int 51 | '''API 返回的状态码''' 52 | response: _FailedResponse 53 | '''API 返回的响应''' 54 | def __init__(self, api: str, status_code: int, response: _FailedResponse) -> None: 55 | '''初始化''' 56 | self.api = api 57 | self.status_code = status_code 58 | self.response = response 59 | 60 | super().__init__(f"API '{api}' failed ({status_code}) : {response['data']}") 61 | 62 | return 63 | 64 | def json(self) -> _FailedResponse: 65 | '''返回 API 返回的响应''' 66 | return self.response 67 | 68 | @property 69 | def data(self) -> str: 70 | return self.response['data'] 71 | 72 | class RoomSubmitFailure(TsuguException): 73 | '''房间号上传失败''' 74 | def __init__(self, response: str) -> None: 75 | '''初始化''' 76 | super().__init__(response) 77 | return 78 | 79 | class RoomQueryFailure(TsuguException): 80 | '''房间号获取失败''' 81 | def __init__(self, response: str) -> None: 82 | '''初始化''' 83 | super().__init__(response) 84 | return 85 | -------------------------------------------------------------------------------- /tsugu_api/_user.py: -------------------------------------------------------------------------------- 1 | 2 | from tsugu_api_core._network import Api 3 | from tsugu_api_core._settings import settings 4 | from tsugu_api_core._typing import ( 5 | ServerId, 6 | _BindingAction, 7 | PartialTsuguUser, 8 | _GetUserDataResponse, 9 | _ChangeUserDataResponse, 10 | _BindPlayerRequestResponse, 11 | _BindPlayerVerificationResponse 12 | ) 13 | 14 | def get_user_data(platform: str, user_id: str) -> _GetUserDataResponse: 15 | '''获取用户数据 16 | 17 | 参数: 18 | platform (str): 平台名称 19 | user_id (str): 用户 ID 20 | 21 | 返回: 22 | _GetUserDataResponse: API 返回响应 23 | ''' 24 | 25 | # 构建数据 26 | data = { 27 | 'platform': platform, 28 | 'userId': user_id 29 | } 30 | 31 | # 发送请求 32 | return Api( 33 | settings.userdata_backend_url, 34 | '/user/getUserData', 35 | proxy=settings.userdata_backend_proxy 36 | ).post(data).json() 37 | 38 | def change_user_data(platform: str, user_id: str, update: PartialTsuguUser) -> _ChangeUserDataResponse: 39 | '''修改用户数据 40 | 41 | 参数: 42 | platform (str): 平台名称 43 | user_id (str): 用户 ID 44 | update (_PartialTsuguUser): 更新数据 45 | 46 | 返回: 47 | _ChangeUserDataResponse: API 返回响应 48 | ''' 49 | 50 | # 构建数据 51 | data = { 52 | 'platform': platform, 53 | 'userId': user_id, 54 | 'update': update 55 | } 56 | 57 | # 发送请求 58 | return Api( 59 | settings.userdata_backend_url, 60 | '/user/changeUserData', 61 | proxy=settings.userdata_backend_proxy 62 | ).post(data).json() 63 | 64 | def bind_player_request( 65 | platform: str, 66 | user_id: str 67 | ) -> _BindPlayerRequestResponse: 68 | '''绑定玩家请求 69 | 70 | 参数: 71 | platform (str): 平台名称 72 | user_id (str): 用户 ID 73 | 74 | 返回: 75 | _BindPlayerRequestResponse: 请求返回数据 76 | ''' 77 | 78 | # 构建数据 79 | data = { 80 | 'platform': platform, 81 | 'userId': user_id 82 | } 83 | 84 | # 发送请求 85 | return Api( 86 | settings.userdata_backend_url, 87 | '/user/bindPlayerRequest', 88 | proxy=settings.userdata_backend_proxy, 89 | ).post(data).json() 90 | 91 | def bind_player_verification( 92 | platform: str, 93 | user_id: str, 94 | server: ServerId, 95 | player_id: int, 96 | binding_action: _BindingAction 97 | ) -> _BindPlayerVerificationResponse: 98 | '''绑定玩家验证 99 | 100 | 参数: 101 | platform (str): 平台名称 102 | user_id (str): 用户 ID 103 | server (_ServerId): 服务器编号 0 - 日服 1 - 国际服 2 - 台服 3 - 国服 4 - 韩服 104 | player_id (int): 玩家 ID 105 | binding_action (_BindingAction): 绑定操作 106 | 107 | 返回: 108 | _BindPlayerVerificationResponse: 验证返回数据 109 | ''' 110 | 111 | # 构建数据 112 | data = { 113 | 'platform': platform, 114 | 'userId': user_id, 115 | 'server': server, 116 | 'playerId': player_id, 117 | 'bindingAction': binding_action 118 | } 119 | 120 | # 发送请求 121 | return Api( 122 | settings.userdata_backend_url, 123 | '/user/bindPlayerVerification', 124 | proxy=settings.userdata_backend_proxy 125 | ).post(data).json() 126 | -------------------------------------------------------------------------------- /tsugu_api_async/_user.py: -------------------------------------------------------------------------------- 1 | 2 | from tsugu_api_core._network import Api 3 | from tsugu_api_core._settings import settings 4 | from tsugu_api_core._typing import ( 5 | ServerId, 6 | _BindingAction, 7 | PartialTsuguUser, 8 | _GetUserDataResponse, 9 | _ChangeUserDataResponse, 10 | _BindPlayerRequestResponse, 11 | _BindPlayerVerificationResponse 12 | ) 13 | 14 | async def get_user_data(platform: str, user_id: str) -> _GetUserDataResponse: 15 | '''获取用户数据 16 | 17 | 参数: 18 | platform (str): 平台名称 19 | user_id (str): 用户 ID 20 | 21 | 返回: 22 | _GetUserDataResponse: API 返回响应 23 | ''' 24 | 25 | # 构建数据 26 | data = { 27 | 'platform': platform, 28 | 'userId': user_id 29 | } 30 | 31 | # 发送请求 32 | return (await Api( 33 | settings.userdata_backend_url, 34 | '/user/getUserData', 35 | proxy=settings.userdata_backend_proxy 36 | ).apost(data)).json() 37 | 38 | async def change_user_data(platform: str, user_id: str, update: PartialTsuguUser) -> _ChangeUserDataResponse: 39 | '''修改用户数据 40 | 41 | 参数: 42 | platform (str): 平台名称 43 | user_id (str): 用户 ID 44 | update (_PartialTsuguUser): 更新数据 45 | 46 | 返回: 47 | _ChangeUserDataResponse: API 返回响应 48 | ''' 49 | 50 | # 构建数据 51 | data = { 52 | 'platform': platform, 53 | 'userId': user_id, 54 | 'update': update 55 | } 56 | 57 | # 发送请求 58 | return (await Api( 59 | settings.userdata_backend_url, 60 | '/user/changeUserData', 61 | proxy=settings.userdata_backend_proxy 62 | ).apost(data)).json() 63 | 64 | async def bind_player_request( 65 | platform: str, 66 | user_id: str 67 | ) -> _BindPlayerRequestResponse: 68 | '''绑定玩家请求 69 | 70 | 参数: 71 | platform (str): 平台名称 72 | user_id (str): 用户 ID 73 | 74 | 返回: 75 | _BindPlayerRequestResponse: 请求返回数据 76 | ''' 77 | 78 | # 构建数据 79 | data = { 80 | 'platform': platform, 81 | 'userId': user_id 82 | } 83 | 84 | # 发送请求 85 | return (await Api( 86 | settings.userdata_backend_url, 87 | '/user/bindPlayerRequest', 88 | proxy=settings.userdata_backend_proxy, 89 | ).apost(data)).json() 90 | 91 | async def bind_player_verification( 92 | platform: str, 93 | user_id: str, 94 | server: ServerId, 95 | player_id: int, 96 | binding_action: _BindingAction 97 | ) -> _BindPlayerVerificationResponse: 98 | '''绑定玩家验证 99 | 100 | 参数: 101 | platform (str): 平台名称 102 | user_id (str): 用户 ID 103 | server (_ServerId): 服务器编号 0 - 日服 1 - 国际服 2 - 台服 3 - 国服 4 - 韩服 104 | player_id (int): 玩家 ID 105 | binding_action (_BindingAction): 绑定操作 106 | 107 | 返回: 108 | _BindPlayerVerificationResponse: 验证返回数据 109 | ''' 110 | 111 | # 构建数据 112 | data = { 113 | 'platform': platform, 114 | 'userId': user_id, 115 | 'server': server, 116 | 'playerId': player_id, 117 | 'bindingAction': binding_action 118 | } 119 | 120 | # 发送请求 121 | return (await Api( 122 | settings.userdata_backend_url, 123 | '/user/bindPlayerVerification', 124 | proxy=settings.userdata_backend_proxy 125 | ).apost(data)).json() 126 | -------------------------------------------------------------------------------- /tsugu_api_core/_typing.py: -------------------------------------------------------------------------------- 1 | ''' 2 | `tsugu_api_core._typing` 3 | 4 | 定义了一些类型别名 5 | ''' 6 | 7 | from typing import ( 8 | Any, 9 | Dict, 10 | List, 11 | Union, 12 | Literal, 13 | TypeAlias, 14 | TypedDict, 15 | TypeGuard, 16 | ) 17 | from typing_extensions import NotRequired 18 | 19 | ServerId: TypeAlias = Literal[0, 1, 2, 3, 4] 20 | ''' 21 | 服务器 ID 22 | 23 | 值: 24 | 0: 日服 25 | 1: 国际服 26 | 2: 台服 27 | 3: 国服 28 | 4: 韩服 29 | ''' 30 | SERVER_ID = (0, 1, 2, 3, 4) 31 | ServerName: TypeAlias = Literal['jp', 'en', 'tw', 'cn', 'kr'] 32 | ''' 33 | 服务器名 34 | 35 | 值: 36 | 'jp': 日服 37 | 'en': 国际服 38 | 'tw': 台服 39 | 'cn': 国服 40 | 'kr': 韩服 41 | ''' 42 | SERVER_NAME = ('jp', 'en', 'tw', 'cn', 'kr') 43 | Server: TypeAlias = Union[ServerId, ServerName] 44 | '''服务器''' 45 | SERVER = (0, 1, 2, 3, 4, 'jp', 'en', 'tw', 'cn', 'kr') 46 | 47 | def is_server(server: Any) -> TypeGuard[Server]: 48 | return server in SERVER 49 | 50 | def is_server_list(server_list: List[Any]) -> TypeGuard[List[Server]]: 51 | for server in server_list: 52 | if not is_server(server): 53 | return False 54 | return True 55 | 56 | class _Data(TypedDict): 57 | '''API 单个响应数据''' 58 | type: Literal['string', 'base64'] 59 | string: str 60 | 61 | _Response: TypeAlias = List[_Data] 62 | 63 | class _Error(TypedDict): 64 | '''错误信息''' 65 | type: str 66 | '''错误类型''' 67 | location: Literal['body', 'cookies', 'headers', 'params', 'query'] 68 | '''参数位置''' 69 | msg: str 70 | '''错误信息''' 71 | path: str 72 | '''参数名称''' 73 | value: Any 74 | '''参数值''' 75 | 76 | class _BadRequestResponse(TypedDict): 77 | '''参数错误响应信息''' 78 | status: Literal['failed'] 79 | data: str 80 | error: List[_Error] 81 | 82 | class _FailedResponse(TypedDict): 83 | '''失败响应信息''' 84 | status: Literal['failed'] 85 | data: str 86 | 87 | _Status: TypeAlias = Literal['success', 'failed'] 88 | '''响应状态''' 89 | 90 | FuzzySearchResult: TypeAlias = Dict[str, List[Union[str, int]]] 91 | 92 | class _FuzzySearchResponse(TypedDict): 93 | '''`/fuzzySearch` 响应结果''' 94 | status: _Status 95 | data: FuzzySearchResult 96 | 97 | _DifficultyId: TypeAlias = Literal[0, 1, 2, 3, 4] 98 | '''难度 ID''' 99 | 100 | class _UserPlayerInList(TypedDict): 101 | playerId: int 102 | server: ServerId 103 | 104 | class _SubmitResponse(TypedDict): 105 | '''`/station/submitRoomNumber` 响应结果''' 106 | status: _Status 107 | data: str 108 | 109 | class _Room(TypedDict): 110 | '''房间数据''' 111 | number: int 112 | rawMessage: str 113 | source: str 114 | userId: str 115 | time: int 116 | player: NotRequired[_UserPlayerInList] 117 | avatarUrl: NotRequired[str] 118 | userName: NotRequired[str] 119 | 120 | class _QueryResponse(TypedDict): 121 | '''`/station/queryAllRoom` 响应结果''' 122 | status: _Status 123 | data: List[_Room] 124 | 125 | class _TsuguUserServer(TypedDict): 126 | '''服务器数据''' 127 | playerId: int 128 | bindingStatus: Literal[0, 1, 2] 129 | verifyCode: NotRequired[int] 130 | 131 | class _TsuguUser(TypedDict): 132 | '''用户数据''' 133 | userId: str 134 | '''用户 ID''' 135 | platform: str 136 | '''平台''' 137 | mainServer: ServerId 138 | '''主服务器''' 139 | displayedServerList: List[ServerId] 140 | '''显示的服务器列表''' 141 | shareRoomNumber: bool 142 | '''是否分享房间号''' 143 | userPlayerIndex: int 144 | '''用户账号索引''' 145 | userPlayerList: List[_UserPlayerInList] 146 | '''用户账号列表''' 147 | 148 | class _GetUserDataResponse(TypedDict): 149 | '''`/user/getUserData` 响应结果''' 150 | status: _Status 151 | data: _TsuguUser 152 | 153 | class PartialTsuguUser(TypedDict): 154 | '''更新数据''' 155 | userId: NotRequired[str] 156 | '''用户 ID''' 157 | platform: NotRequired[str] 158 | '''平台''' 159 | mainServer: NotRequired[ServerId] 160 | '''主服务器''' 161 | displayedServerList: NotRequired[List[ServerId]] 162 | '''显示的服务器列表''' 163 | shareRoomNumber: NotRequired[bool] 164 | '''是否分享房间号''' 165 | userPlayerIndex: NotRequired[int] 166 | '''用户账号索引''' 167 | userPlayerList: NotRequired[List[_UserPlayerInList]] 168 | '''用户账号列表''' 169 | 170 | class _ChangeUserDataResponse(TypedDict): 171 | '''`/user/changeUserData` 响应结果''' 172 | status: _Status 173 | 174 | class _VerifyCode(TypedDict): 175 | '''验证码''' 176 | verifyCode: int 177 | 178 | _BindingAction: TypeAlias = Literal['bind', 'unbind'] 179 | '''绑定操作''' 180 | 181 | class _BindPlayerRequestResponse(TypedDict): 182 | '''`/user/bindPlayerRequest` 绑定响应''' 183 | status: _Status 184 | data: _VerifyCode 185 | 186 | class _BindPlayerVerificationResponse(TypedDict): 187 | '''`/user/bindPlayerVerification` 响应结果''' 188 | status: _Status 189 | data: str 190 | 191 | class _SourceInfo(TypedDict): 192 | '''来源信息''' 193 | name: str 194 | type: str 195 | 196 | class _UserInfo(TypedDict): 197 | '''用户信息''' 198 | avatar: str 199 | bandori_player_brief_info: Any 200 | role: int 201 | type: str 202 | user_id: int 203 | username: str 204 | 205 | class StationRoom(TypedDict): 206 | '''车站房间数据''' 207 | number: int 208 | raw_message: str 209 | source_info: _SourceInfo 210 | time: int 211 | type: str 212 | user_info: _UserInfo 213 | -------------------------------------------------------------------------------- /tsugu_api_core/client/httpx.py: -------------------------------------------------------------------------------- 1 | '''内置的 HTTPX 客户端适配''' 2 | 3 | import time 4 | import asyncio 5 | from json import dumps 6 | from typing import Any, cast 7 | from typing_extensions import override 8 | 9 | from ._client import Client as _Client 10 | from ._client import Request, Response 11 | 12 | try: 13 | import httpx 14 | except ModuleNotFoundError as exception: 15 | raise ImportError( 16 | 'module \'httpx\' is not installed, please install it by running \'pip install httpx\'' 17 | ) from exception 18 | 19 | __HTTPX_ABOVE_0_28_0__ : bool = tuple(httpx.__version__.split('.')) >= ('0', '28', '0') 20 | 21 | class Client(_Client): 22 | '''HTTPX 客户端适配''' 23 | _client: httpx.Client 24 | '''HTTPX 客户端''' 25 | _async_client: httpx.AsyncClient 26 | '''异步 HTTPX 客户端''' 27 | 28 | @override 29 | def __enter__(self) -> 'Client': 30 | if __HTTPX_ABOVE_0_28_0__: 31 | self._client = httpx.Client( 32 | proxy=self.proxy, 33 | timeout=self.timeout, 34 | trust_env=False, 35 | ) 36 | else: 37 | proxies = { 38 | 'http://': self.proxy, 39 | 'https://': self.proxy, 40 | } if self.proxy else None 41 | 42 | self._client = httpx.Client( 43 | proxies=cast(dict, proxies), 44 | timeout=self.timeout, 45 | trust_env=False, 46 | ) 47 | 48 | self._client.__enter__() 49 | return self 50 | 51 | @override 52 | async def __aenter__(self) -> 'Client': 53 | if __HTTPX_ABOVE_0_28_0__: 54 | self._async_client = httpx.AsyncClient( 55 | proxy=self.proxy, 56 | timeout=self.timeout, 57 | trust_env=False, 58 | ) 59 | else: 60 | proxies = { 61 | 'http://': self.proxy, 62 | 'https://': self.proxy, 63 | } if self.proxy else None 64 | 65 | self._async_client = httpx.AsyncClient( 66 | proxies=cast(dict, proxies), 67 | timeout=self.timeout, 68 | trust_env=False, 69 | ) 70 | 71 | await self._async_client.__aenter__() 72 | return self 73 | 74 | @override 75 | def __exit__(self, *args: Any) -> None: 76 | self._client.__exit__(*args) 77 | 78 | @override 79 | async def __aexit__(self, *args: Any) -> None: 80 | await self._async_client.__aexit__(*args) 81 | 82 | @override 83 | def request(self, request: Request) -> Response: 84 | if __HTTPX_ABOVE_0_28_0__: 85 | _request = httpx.Request( 86 | request.method, 87 | request.url, 88 | params=request.params, 89 | content=cast(dict, dumps(request.data)) if request.data is not None else request.data, 90 | headers=request.headers, 91 | ) 92 | else: 93 | _request = httpx.Request( 94 | request.method, 95 | request.url, 96 | params=request.params, 97 | data=cast(dict, dumps(request.data)) if request.data is not None else request.data, 98 | headers=request.headers, 99 | ) 100 | 101 | retries = 0 102 | while True: 103 | try: 104 | _response = self._client.send(_request) 105 | break 106 | except Exception as exception: 107 | if retries >= self.max_retries: 108 | raise exception 109 | retries += 1 110 | time.sleep(0.5) 111 | 112 | # 处理响应 113 | try: 114 | _response.raise_for_status() 115 | return Response( 116 | _response.content, 117 | _response.status_code, 118 | ) 119 | except httpx.HTTPStatusError as exception: 120 | return Response( 121 | exception.response.content, 122 | exception.response.status_code, 123 | exception, 124 | ) 125 | 126 | @override 127 | async def arequest(self, request: Request) -> Response: 128 | if __HTTPX_ABOVE_0_28_0__: 129 | _request = httpx.Request( 130 | request.method, 131 | request.url, 132 | params=request.params, 133 | content=cast(dict, dumps(request.data)) if request.data is not None else request.data, 134 | headers=request.headers, 135 | ) 136 | else: 137 | _request = httpx.Request( 138 | request.method, 139 | request.url, 140 | params=request.params, 141 | data=cast(dict, dumps(request.data)) if request.data is not None else request.data, 142 | headers=request.headers, 143 | ) 144 | 145 | retries = 0 146 | while True: 147 | try: 148 | _response = await self._async_client.send(_request) 149 | break 150 | except Exception as exception: 151 | if retries >= self.max_retries: 152 | raise exception 153 | retries += 1 154 | await asyncio.sleep(0.5) 155 | 156 | # 处理响应 157 | try: 158 | _response.raise_for_status() 159 | return Response( 160 | _response.content, 161 | _response.status_code, 162 | ) 163 | except httpx.HTTPStatusError as exception: 164 | return Response( 165 | exception.response.content, 166 | exception.response.status_code, 167 | exception, 168 | ) -------------------------------------------------------------------------------- /tsugu_api_core/_network.py: -------------------------------------------------------------------------------- 1 | '''`tsugu_api_async._network` 2 | 3 | 向 Tsugu 后端发送请求相关模块''' 4 | from typing import Any, Literal, Optional 5 | 6 | from ._settings import settings 7 | from .client import _Client, Request, Response 8 | from .exception import BadRequestError, FailedException, HTTPStatusError 9 | 10 | BANDORI_STATION_URL = 'https://api.bandoristation.com' 11 | 12 | # 向后端发送 API 请求类 13 | class Api: 14 | '''向后端发送 API 请求类 15 | 16 | 参数: 17 | host (str): 请求的主机地址 18 | api (str): 请求的 API 路径 19 | proxy (bool): 是否使用代理服务器 20 | ''' 21 | host: str 22 | '''请求的主机地址''' 23 | api: str 24 | '''请求的 API 路径''' 25 | proxy: bool 26 | '''是否使用代理服务器''' 27 | # 初始化 28 | def __init__( 29 | self, 30 | host: str, 31 | api: str, 32 | proxy: bool 33 | ) -> None: 34 | '''初始化''' 35 | self.host = host.rstrip('/') 36 | self.api = api 37 | self.proxy = proxy 38 | return 39 | 40 | # 请求发送 41 | async def _arequest( 42 | self, 43 | method: Literal['get', 'post'], 44 | *, 45 | params: Optional[dict[str, Any]]=None, 46 | data: Optional[dict[str, Any]]=None 47 | ) -> Response: 48 | '''异步请求发送 49 | 50 | 参数: 51 | method (Literal['get', 'post']): API 调用方法 52 | params (Optional[dict[str, Any]]): 请求的参数 53 | data (Optional[dict[str, Any]]): 请求的数据 54 | 55 | 返回: 56 | Response: 收到的响应 57 | ''' 58 | # 构建请求头 59 | if method == 'post': 60 | headers = {'Content-Type': 'application/json'} 61 | else: 62 | headers = None 63 | 64 | # 发送请求并获取响应 65 | async with _Client()( 66 | proxy=settings.proxy if self.proxy and settings.proxy else None, 67 | timeout=settings.timeout, 68 | max_retries=settings.max_retries, 69 | ) as client: 70 | # 构建一个请求体 71 | request = Request( 72 | method, 73 | self.host + self.api, 74 | params=params, 75 | data=data, 76 | headers=headers 77 | ) 78 | 79 | response = await client.arequest(request) 80 | 81 | # 处理接收到的响应 82 | if response.status_code == 200: 83 | return response 84 | if response.status_code == 400: 85 | if self.host == BANDORI_STATION_URL: 86 | return response 87 | _response: dict[str, Any] = response.json() 88 | raise BadRequestError(self.api, _response) from response.exception # type: ignore 89 | elif response.status_code in (404, 409, 422, 500): 90 | _response: dict[str, Any] = response.json() 91 | raise FailedException(self.api, response.status_code, _response) from response.exception # type: ignore 92 | else: 93 | raise HTTPStatusError(response.status_code) from response.exception 94 | 95 | async def apost(self, data: Optional[dict[str, Any]]=None) -> Response: 96 | '''异步发送 POST 请求 97 | 98 | 参数: 99 | data (Optional[dict[str, Any]]): 请求的数据 100 | 101 | 返回: 102 | Response: 收到的响应 103 | ''' 104 | return await self._arequest('post', data=data) 105 | 106 | async def aget(self, params: Optional[dict[str, Any]]=None) -> Response: 107 | '''异步发送 GET 请求 108 | 109 | 参数: 110 | params (Optional[dict[str, Any]]): 请求的参数 111 | 112 | 返回: 113 | _ApiResponse: 收到的响应 114 | ''' 115 | return await self._arequest('get', params=params) 116 | 117 | # 请求发送 118 | def _request( 119 | self, 120 | method: Literal['get', 'post'], 121 | *, 122 | params: Optional[dict[str, Any]]=None, 123 | data: Optional[dict[str, Any]]=None 124 | ) -> Response: 125 | '''请求发送 126 | 127 | 参数: 128 | method (Literal['get', 'post']): API 调用方法 129 | params (Optional[dict[str, Any]]): 请求的参数 130 | data (Optional[dict[str, Any]]): 请求的数据 131 | 132 | 返回: 133 | Response: 收到的响应 134 | ''' 135 | # 构建请求头 136 | if method == 'post': 137 | headers = {'Content-Type': 'application/json'} 138 | else: 139 | headers = None 140 | 141 | # 构建一个请求体 142 | request = Request( 143 | method, 144 | self.host + self.api, 145 | params=params, 146 | data=data, 147 | headers=headers 148 | ) 149 | 150 | # 发送请求并获取响应 151 | with _Client()( 152 | proxy=settings.proxy if self.proxy and settings.proxy else None, 153 | timeout=settings.timeout, 154 | max_retries=settings.max_retries, 155 | ) as client: 156 | response = client.request(request) 157 | 158 | # 处理接收到的响应 159 | if response.status_code == 200: 160 | return response 161 | if response.status_code == 400: 162 | if self.host == BANDORI_STATION_URL: 163 | return response 164 | _response: dict[str, Any] = response.json() 165 | raise BadRequestError(self.api, _response) from response.exception # type: ignore 166 | elif response.status_code in (409, 422, 500): 167 | _response: dict[str, Any] = response.json() 168 | raise FailedException(self.api, response.status_code, _response) from response.exception # type: ignore 169 | else: 170 | raise HTTPStatusError(response.status_code) from response.exception 171 | 172 | def post(self, data: Optional[dict[str, Any]]=None) -> Response: 173 | '''发送 POST 请求 174 | 175 | 参数: 176 | data (Optional[dict[str, Any]]): 请求的数据 177 | 178 | 返回: 179 | Response: 收到的响应 180 | ''' 181 | return self._request('post', data=data) 182 | 183 | def get(self, params: Optional[dict[str, Any]]=None) -> Response: 184 | '''发送 GET 请求 185 | 186 | 参数: 187 | params (Optional[dict[str, Any]]): 请求的参数 188 | 189 | 返回: 190 | Response: 收到的响应 191 | ''' 192 | return self._request('get', params=params) 193 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | ![tsugu-api-python logo](https://raw.githubusercontent.com/WindowsSov8forUs/tsugu-api-python/refs/heads/main/logo.png) 4 | 5 | # tsugu-api-python 6 | 7 | _✨ Python 编写的 [TsuguBanGDreamBot](https://github.com/Yamamoto-2/tsugu-bangdream-bot?tab=readme-ov-file) 相关各种功能 API 调用库 ✨_ 8 | 9 |
10 | 11 |

12 | 13 | 14 | license 15 | 16 | 17 | 18 | Latest Release Version 19 | 20 | 21 | 22 | License 23 | 24 | 25 | 26 | Python Version 27 | 28 | 29 | 30 | PyPI Version 31 | 32 | 33 | 34 | PyPI Version 35 | 36 | 37 |

38 | 39 | ## 说明 40 | 41 | 这是一个用 Python 编写的调用 [TsuguBanGDreamBot](https://github.com/Yamamoto-2/tsugu-bangdream-bot?tab=readme-ov-file) 相关各种功能 API 的库,包括绝大部分 Tsugu 提供的功能。使用本 API 库提供的方法可以实现绝大部分功能,而搭配 [bestdori-api](https://github.com/WindowsSov8forUs/bestdori-api) 可以实现用户绑定等其他功能。 42 | 43 | > 该 API 库同时提供了异步与同步版本,可自行选择使用。 44 | 45 | > 一切数据获取等操作通过配置的后端服务器进行,该 API 库只提供前端所需的调用功能。若需要使用本地数据库,请自行操作。 46 | 47 | ### 目前已有的功能 48 | 49 | > 所有方法都同时拥有异步与同步版本。 50 | 51 | #### Tsugu 后端功能 52 | 53 | |功能描述|方法名称| 54 | |:------|:----------| 55 | |获取活动试炼舞台信息|`event_stage`| 56 | |模拟指定卡池抽卡结果|`gacha_simulate`| 57 | |获取卡面图片|`get_card_illustration`| 58 | |查询指定活动指定档位相关的历史预测线|`lsycx(deprecated)`/`cutoff_list_of_recent_event`| 59 | |获取指定车牌列表的图片形式|`room_list`| 60 | |查询符合条件的卡牌|`search_card`| 61 | |查询符合条件的角色信息|`search_character`| 62 | |查询符合条件的活动信息|`search_event`| 63 | |查询指定卡池信息|`search_gacha`| 64 | |获取玩家状态信息|`search_player`| 65 | |查询符合条件的歌曲信息|`search_song`| 66 | |查询指定歌曲指定难度的谱面|`song_chart`| 67 | |查询歌曲分数表|`song_meta`| 68 | |查询指定活动的指定档位预测线|`ycx(deprecated)`/`cutoff_detail`| 69 | |查询指定活动的全部档位预测线|`ycx_all(deprecated)`/`cutoff_all`| 70 | 71 | #### 车站数据后端功能 72 | 73 | |功能描述|方法名称| 74 | |:------|:----------| 75 | |提交房间信息到后端|`station_submit_room_number`| 76 | |从后端获取最近的房间信息列表|`station_query_all_room`| 77 | 78 | > 若后端不支持用户数据库,以上功能可能无法使用,请以 **车站功能** API 代替。 79 | 80 | #### 用户数据后端功能 81 | 82 | |功能描述|方法名称| 83 | |:------|:----------| 84 | |获取用户数据|`get_user_data`| 85 | |修改用户数据|`change_user_data`| 86 | |发送绑定用户请求|`bind_player_request`| 87 | |验证绑定用户请求|`bind_player_verification`| 88 | 89 | > 以上功能都可使用本地用户数据库代替,本 API 不提供相关的配置方法。 90 | 91 | #### 车站功能 92 | 93 | |功能描述|方法名称| 94 | |:------|:----------| 95 | |从车站获取最近的房间信息列表|`query_room_number`| 96 | |提交房间信息到车站|`submit_room_number`| 97 | 98 | 99 | ## 快速使用 100 | 101 | 以下将以获取歌曲 **EXIST** (id=325) 的信息为例。 102 | 103 | 使用以下指令安装本模块: 104 | ```bash 105 | $ pip3 install tsugu-api-python 106 | ``` 107 | 108 | > 使用**可选依赖**安装可同时安装指定 HTTP 请求库 109 | > 110 | > ```bash 111 | > $ pip3 install tsugu-api-python[httpx] # 同时安装适配版本 httpx 112 | > $ pip3 install tsugu-api-python[aiohttp] # 同时安装适配版本 aiohttp 113 | > ``` 114 | 115 | 使用如下代码,获取指定歌曲信息图片: 116 | 117 | ```python 118 | from tsugu_api import search_song 119 | 120 | def main() -> None: 121 | result = search_song([3, 0], "EXIST") # 这里也可以传入 "325" ,具体取决于用户输入信息 122 | 123 | main() 124 | ``` 125 | 126 | > `[3, 0]` 指代用户的默认服务器列表,可从通过 `get_user_data()` 方法获取的返回值中获取。 127 | 128 | 获取到的 `result` 将是一个 `_Response` 对象,当获取到准确的信息时, `result` 的值如下: 129 | 130 | ```python 131 | [ 132 | { 133 | "type": "base64", 134 | "string": ... # 图片的 Base64 字符串 135 | } 136 | ] 137 | ``` 138 | 139 | 若传入的查询参数不合法或查询过程中出错,获取到的 `result` 的值如下: 140 | 141 | ```python 142 | [ 143 | { 144 | "type": "string", 145 | "string": ... # 错误信息 146 | } 147 | ] 148 | ``` 149 | 150 | > 异步版本的调用方式相同,只是将 `tsugu_api` 改为 `tsugu_api_async` 即可。 151 | 152 | ### 注册自定义 HTTP 客户端 153 | 154 | 在 `1.5.0` 以后版本, `tsugu-api-python` 将不再强制要求安装 `httpx` 与 `aiohttp` 库,并允许用户自己实现用于请求的 HTTP 客户端。 `tsugu-api-python` 内部提供对于 `httpx` 与 `aiohttp` 的客户端实现。 155 | 156 | 用户可通过实现 `tsugu_api_core.client.Client` 类,并通过 `tsugu_api_core.register_client` 方法注册。注册后 `tsugu-api-python` 将不会根据 `settings` 中的 `client` 配置选择客户端实现,而是使用用户提供的自定义客户端实现。 157 | 158 | 以下给出一个实现基于 `requests` 请求库的客户端实现。 159 | 160 | ```python 161 | # 客户端实现 162 | 163 | # 导入一些需要的库 164 | from json import dumps 165 | from typing import Any, cast 166 | from typing_extensions import override 167 | 168 | # 导入 requests 库 169 | import requests 170 | 171 | # 导入 Client 基类与 Request 、 Response 类 172 | from tsugu_api_core.client import Client as _Client 173 | from tsugu_api_core.client import Request, Response 174 | 175 | # 实现客户端类 176 | class Client(_Client): 177 | _session: requests.Session 178 | 179 | @override 180 | def __enter__(self) -> 'Client': 181 | self._client = requests.Session() 182 | self._client.trust_env = True 183 | self._client.__enter__() 184 | return self 185 | 186 | @override 187 | async def __aenter__(self) -> 'Client': 188 | # requests 库是一个同步请求库,但客户端需要进行异步实现。这里抛出异常来指出这个错误使用。 189 | raise RuntimeError('REQUESTS client is not asynchronous, please use sync context manager') 190 | 191 | @override 192 | def __exit__(self, *args: Any) -> None: 193 | self._session.close() 194 | 195 | @override 196 | async def __aexit__(self, *args: Any) -> None: 197 | pass # 这里这个方法永远不会被使用 198 | 199 | @override 200 | def request(self, request: Request) -> Response: 201 | # requests 需要一个代理字典,但 Client 类只会传入一个代理地址,这里构建这个字典。 202 | # 此处实现根据用户需求不同进行不同实现。 203 | proxies = { 204 | 'http': self.proxy, 205 | 'https': self.proxy, 206 | } 207 | 208 | # 此处请求实现根据用户需求不同而进行不同实现 209 | # 这里列举出了 Request 类的所有可拥有属性 210 | with self._session.request( 211 | request.method, 212 | request.url, 213 | params=request.params, 214 | data=cast(dict, dumps(request.data)) if request.data is not None else request.data, 215 | headers=request.headers, 216 | proxies=proxies if self.proxy else None, 217 | ) as response: 218 | # 请求后构建 Response 类并返回 219 | # - content: 响应内容, bytes 类型 220 | # - status_code: 状态码 221 | # - exception: raise_for_status 方法抛出的错误 222 | try: 223 | response.raise_for_status() 224 | return Response( 225 | response.content, 226 | response.status_code, 227 | ) 228 | except Exception as exception: 229 | return Response( 230 | response.content, 231 | response.status_code, 232 | exception, 233 | ) 234 | 235 | @override 236 | async def arequest(self, request: Request) -> Response: 237 | # requests 库是一个同步请求库,但客户端需要进行异步实现。这里抛出异常来指出这个错误使用。 238 | raise RuntimeError('REQUESTS client is not asynchronous, please use sync request method.') 239 | ``` 240 | 241 | 在客户端实现后,通过注册方法注册客户端。 242 | 243 | ```python 244 | from tsugu_api_core import register_client 245 | 246 | register_client(Client) 247 | ``` 248 | 249 | 随后 `tsugu-api-python` 就会在进行同步请求时优先使用自定义的基于 `requests` 请求库的客户端,而不是内置的基于 `httpx` 库的客户端。 -------------------------------------------------------------------------------- /tsugu_api/_tsugu.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | from typing_extensions import deprecated 3 | from typing import List, Optional, Sequence 4 | 5 | from tsugu_api_core._network import Api 6 | from tsugu_api_core._settings import settings 7 | from tsugu_api_core._typing import ( 8 | _Room, 9 | Server, 10 | ServerId, 11 | _Response, 12 | _DifficultyId, 13 | FuzzySearchResult, 14 | _FuzzySearchResponse 15 | ) 16 | 17 | def cutoff_all(main_server: ServerId, event_id: Optional[int] = None) -> _Response: 18 | '''查询活动排行榜全部预测线 19 | 20 | 参数: 21 | main_server (_ServerId): 主服务器 22 | event_id (int): 活动 ID 23 | 24 | 返回: 25 | _Response: 响应信息 26 | ''' 27 | 28 | # 构建数据 29 | data = { 30 | 'mainServer': main_server, 31 | 'compress': settings.compress 32 | } 33 | if event_id: 34 | data['eventId'] = event_id 35 | 36 | # 发送请求 37 | return Api( 38 | settings.backend_url, 39 | '/cutoffAll', 40 | proxy=settings.backend_proxy 41 | ).post(data).json() 42 | 43 | def cutoff_detail(main_server: ServerId, tier: int, event_id: Optional[int] = None) -> _Response: 44 | '''查询活动排行榜预测线 45 | 46 | 参数: 47 | main_server (_ServerId): 主服务器 48 | tier (int): 排行榜挡位 49 | event_id (int): 活动 ID 50 | 51 | 返回: 52 | _Response: 响应信息 53 | ''' 54 | 55 | # 构建数据 56 | data = { 57 | 'mainServer': main_server, 58 | 'tier': tier, 59 | 'compress': settings.compress 60 | } 61 | if event_id: 62 | data['eventId'] = event_id 63 | 64 | # 发送请求 65 | return Api( 66 | settings.backend_url, 67 | '/cutoffDetail', 68 | proxy=settings.backend_proxy 69 | ).post(data).json() 70 | 71 | def cutoff_list_of_recent_event(main_server: ServerId, tier: int, event_id: Optional[int] = None) -> _Response: 72 | '''查询历史活动排行榜预测线 73 | 74 | 参数: 75 | main_server (_ServerId): 主服务器 76 | tier (int): 排行榜挡位 77 | event_id (int): 活动 ID 78 | 79 | 返回: 80 | _Response: 响应信息 81 | ''' 82 | 83 | # 构建数据 84 | data = { 85 | 'mainServer': main_server, 86 | 'tier': tier, 87 | 'compress': settings.compress 88 | } 89 | if event_id: 90 | data['eventId'] = event_id 91 | 92 | # 发送请求 93 | return Api( 94 | settings.backend_url, 95 | '/cutoffListOfRecentEvent', 96 | proxy=settings.backend_proxy 97 | ).post(data).json() 98 | 99 | def event_stage(main_server: ServerId, event_id: Optional[int] = None, meta: bool = False) -> _Response: 100 | '''查询团队 LIVE 佳节活动舞台数据 101 | 102 | 参数: 103 | main_server (_ServerId): 主服务器 104 | event_id (int): 活动 ID 105 | meta (bool): 是否携带歌曲分数表 106 | 107 | 返回: 108 | _Response: 响应信息 109 | ''' 110 | 111 | # 构建数据 112 | data = { 113 | 'mainServer': main_server, 114 | 'meta': meta, 115 | 'compress': settings.compress 116 | } 117 | if event_id: 118 | data['eventId'] = event_id 119 | 120 | # 发送请求 121 | return Api( 122 | settings.backend_url, 123 | '/eventStage', 124 | proxy=settings.backend_proxy 125 | ).post(data).json() 126 | 127 | def fuzzy_search(text: str) -> _FuzzySearchResponse: 128 | '''模糊搜索 129 | 130 | 参数: 131 | text (str): 搜索文本 132 | 133 | 返回: 134 | _FuzzySearchResponse: 响应信息 135 | ''' 136 | 137 | # 构建数据 138 | data = { 139 | 'text': text, 140 | 'compress': settings.compress 141 | } 142 | 143 | # 发送请求 144 | return Api( 145 | settings.backend_url, 146 | '/fuzzySearch', 147 | proxy=settings.backend_proxy 148 | ).post(data).json() 149 | 150 | def gacha_simulate(main_server: ServerId, times: Optional[int] = None, gacha_id: Optional[int] = None) -> _Response: 151 | '''模拟抽卡 152 | 153 | 参数: 154 | main_server (_ServerId): 主服务器 155 | times (int): 抽卡次数 156 | gacha_id (int): 卡池 ID 157 | 158 | 返回: 159 | _Response: 响应信息 160 | ''' 161 | 162 | # 构建数据 163 | data = { 164 | 'mainServer': main_server, 165 | 'compress': settings.compress 166 | } 167 | if times: 168 | data['times'] = times 169 | if gacha_id: 170 | data['gachaId'] = gacha_id 171 | 172 | # 发送请求 173 | return Api( 174 | settings.backend_url, 175 | '/gachaSimulate', 176 | proxy=settings.backend_proxy 177 | ).post(data).json() 178 | 179 | def get_card_illustration(card_id: int) -> _Response: 180 | '''获取卡面 181 | 182 | 参数: 183 | card_id (int): 卡片 ID 184 | 185 | 返回: 186 | _Response: 响应信息 187 | ''' 188 | 189 | # 构建数据 190 | data = { 191 | 'cardId': str(card_id) 192 | } 193 | 194 | # 发送请求 195 | return Api( 196 | settings.backend_url, 197 | '/getCardIllustration', 198 | proxy=settings.backend_proxy 199 | ).post(data).json() 200 | 201 | @deprecated("The `lsycx` api is now deprecated, use `cutoff_list_of_recent_event` instead.", category=None) 202 | def lsycx(server: Server, tier: int, event_id: Optional[int] = None) -> _Response: 203 | '''查询历史排行榜预测线 204 | 205 | 参数: 206 | server (_Server): 服务器 207 | tier (int): 排行榜挡位 208 | event_id (int): 活动 ID 209 | 210 | 返回: 211 | _Response: 响应信息 212 | ''' 213 | warnings.warn( 214 | "The `lsycx` api is now deprecated, use `cutoff_list_of_recent_event` instead.", 215 | DeprecationWarning 216 | ) 217 | 218 | if not isinstance(server, int): 219 | raise ValueError("'server' must be an integer.") 220 | 221 | return cutoff_list_of_recent_event(server, tier, event_id) 222 | 223 | def room_list(room_list: List[_Room]) -> _Response: 224 | '''绘制车牌绘图 225 | 226 | 参数: 227 | room_list (list[_CarData]): 车牌信息列表 228 | 229 | 返回: 230 | _Response: 响应信息 231 | ''' 232 | 233 | # 构建数据 234 | data = { 235 | 'roomList': room_list, 236 | 'compress': settings.compress 237 | } 238 | 239 | # 发送请求 240 | return Api( 241 | settings.backend_url, 242 | '/roomList', 243 | proxy=settings.backend_proxy 244 | ).post(data).json() 245 | 246 | def search_card( 247 | displayed_server_list: Sequence[ServerId], 248 | text: Optional[str] = None, 249 | fuzzy_search_result: Optional[FuzzySearchResult] = None 250 | ) -> _Response: 251 | '''查询卡片 252 | 253 | 参数: 254 | displayed_server_list (Sequence[_ServerId]): 展示服务器 255 | text (str): 查询参数,与 `fuzzy_search_result` 二选一 256 | fuzzy_search_result (_FuzzySearchResult): 模糊搜索结果,与 `text` 二选一 257 | 258 | 返回: 259 | _Response: 响应信息 260 | ''' 261 | 262 | # 构建数据 263 | data = { 264 | 'displayedServerList': displayed_server_list, 265 | 'useEasyBG': settings.use_easy_bg, 266 | 'compress': settings.compress 267 | } 268 | if text: 269 | data['text'] = text 270 | if fuzzy_search_result: 271 | data['fuzzySearchResult'] = fuzzy_search_result 272 | 273 | # 发送请求 274 | return Api( 275 | settings.backend_url, 276 | '/searchCard', 277 | proxy=settings.backend_proxy 278 | ).post(data).json() 279 | 280 | def search_character( 281 | displayed_server_list: Sequence[ServerId], 282 | fuzzy_search_result: Optional[FuzzySearchResult] = None, 283 | text: Optional[str] = None 284 | ) -> _Response: 285 | '''查询角色 286 | 287 | 参数: 288 | displayed_server_list (Sequence[_ServerId]): 展示服务器 289 | fuzzy_search_result (_FuzzySearchResult): 模糊搜索结果,与 `text` 二选一 290 | text (str): 查询参数,与 `fuzzy_search_result` 二选一 291 | 292 | 返回: 293 | _Response: 响应信息 294 | ''' 295 | 296 | # 构建数据 297 | data = { 298 | 'displayedServerList': displayed_server_list, 299 | 'compress': settings.compress 300 | } 301 | if fuzzy_search_result: 302 | data['fuzzySearchResult'] = fuzzy_search_result 303 | if text: 304 | data['text'] = text 305 | 306 | # 发送请求 307 | return Api( 308 | settings.backend_url, 309 | '/searchCharacter', 310 | proxy=settings.backend_proxy 311 | ).post(data).json() 312 | 313 | def search_event( 314 | displayed_server_list: Sequence[ServerId], 315 | fuzzy_search_result: Optional[FuzzySearchResult] = None, 316 | text: Optional[str] = None 317 | ) -> _Response: 318 | '''查询活动 319 | 320 | 参数: 321 | displayed_server_list (Sequence[_ServerId]): 展示服务器 322 | fuzzy_search_result (_FuzzySearchResult): 模糊搜索结果,与 `text` 二选一 323 | text (str): 查询参数,与 `fuzzy_search_result` 二选一 324 | 325 | 返回: 326 | _Response: 响应信息 327 | ''' 328 | 329 | # 构建数据 330 | data = { 331 | 'displayedServerList': displayed_server_list, 332 | 'useEasyBG': settings.use_easy_bg, 333 | 'compress': settings.compress 334 | } 335 | if fuzzy_search_result: 336 | data['fuzzySearchResult'] = fuzzy_search_result 337 | if text: 338 | data['text'] = text 339 | 340 | # 发送请求 341 | return Api( 342 | settings.backend_url, 343 | '/searchEvent', 344 | proxy=settings.backend_proxy 345 | ).post(data).json() 346 | 347 | def search_gacha( 348 | displayed_server_list: Sequence[ServerId], 349 | gacha_id: int 350 | ) -> _Response: 351 | '''查询卡池 352 | 353 | 参数: 354 | displayed_server_list (Sequence[_ServerId]): 展示服务器 355 | gacha_id (int): 卡池 ID 356 | 357 | 返回: 358 | _Response: 响应信息 359 | ''' 360 | 361 | # 构建数据 362 | data = { 363 | 'displayedServerList': displayed_server_list, 364 | 'gachaId': gacha_id, 365 | 'useEasyBG': settings.use_easy_bg, 366 | 'compress': settings.compress 367 | } 368 | 369 | # 发送请求 370 | return Api( 371 | settings.backend_url, 372 | '/searchGacha', 373 | proxy=settings.backend_proxy 374 | ).post(data).json() 375 | 376 | def search_player(player_id: int, main_server: ServerId) -> _Response: 377 | '''查询玩家状态 378 | 379 | 参数: 380 | player_id (int): 玩家 ID 381 | main_server (_ServerId): 服务器 382 | 383 | 返回: 384 | _Response: 响应信息 385 | ''' 386 | 387 | # 构建数据 388 | data = { 389 | 'playerId': player_id, 390 | 'mainServer': main_server, 391 | 'useEasyBG': settings.use_easy_bg, 392 | 'compress': settings.compress 393 | } 394 | 395 | # 发送请求 396 | return Api( 397 | settings.backend_url, 398 | '/searchPlayer', 399 | proxy=settings.backend_proxy 400 | ).post(data).json() 401 | 402 | def search_song( 403 | displayed_server_list: Sequence[ServerId], 404 | fuzzy_search_result: Optional[FuzzySearchResult] = None, 405 | text: Optional[str] = None 406 | ) -> _Response: 407 | '''查询歌曲 408 | 409 | 参数: 410 | displayed_server_list (Sequence[_ServerId]): 展示服务器 411 | fuzzy_search_result (_FuzzySearchResult): 模糊搜索结果,与 `text` 二选一 412 | text (str): 查询参数,与 `fuzzy_search_result` 二选一 413 | 414 | 返回: 415 | _Response: 响应信息 416 | ''' 417 | 418 | # 构建数据 419 | data = { 420 | 'displayedServerList': displayed_server_list, 421 | 'compress': settings.compress 422 | } 423 | if fuzzy_search_result: 424 | data['fuzzySearchResult'] = fuzzy_search_result 425 | if text: 426 | data['text'] = text 427 | 428 | # 发送请求 429 | return Api( 430 | settings.backend_url, 431 | '/searchSong', 432 | proxy=settings.backend_proxy 433 | ).post(data).json() 434 | 435 | def song_chart( 436 | displayed_server_list: Sequence[ServerId], 437 | song_id: int, 438 | difficulty_id: _DifficultyId 439 | ) -> _Response: 440 | '''查询歌曲谱面 441 | 442 | 参数: 443 | displayed_server_list (Sequence[_ServerId]): 展示服务器 444 | song_id (int): 歌曲 ID 445 | difficulty_id (_DifficultyId): 难度 ID 446 | 447 | 返回: 448 | _Response: 响应信息 449 | ''' 450 | 451 | # 构建数据 452 | data = { 453 | 'displayedServerList': displayed_server_list, 454 | 'songId': song_id, 455 | 'difficultyId': difficulty_id, 456 | 'compress': settings.compress 457 | } 458 | 459 | # 发送请求 460 | return Api( 461 | settings.backend_url, 462 | '/songChart', 463 | proxy=settings.backend_proxy 464 | ).post(data).json() 465 | 466 | def song_meta( 467 | displayed_server_list: Sequence[ServerId], 468 | main_server: ServerId 469 | ) -> _Response: 470 | '''查询歌曲分数表 471 | 472 | 参数: 473 | displayed_server_list (Sequence[_ServerId]): 展示服务器 474 | main_server (_ServerId): 主服务器 475 | 476 | 返回: 477 | _Response: 响应信息 478 | ''' 479 | 480 | # 构建数据 481 | data = { 482 | 'displayedServerList': displayed_server_list, 483 | 'mainServer': main_server, 484 | 'compress': settings.compress 485 | } 486 | 487 | # 发送请求 488 | return Api( 489 | settings.backend_url, 490 | '/songMeta', 491 | proxy=settings.backend_proxy 492 | ).post(data).json() 493 | 494 | def song_random( 495 | main_server: ServerId, 496 | fuzzy_search_result: Optional[FuzzySearchResult] = None, 497 | text: Optional[str] = None 498 | ) -> _Response: 499 | '''随机歌曲 500 | 501 | 参数: 502 | main_server (_ServerId): 主服务器 503 | fuzzy_search_result (_FuzzySearchResult): 模糊搜索结果,与 `text` 二选一 504 | text (str): 查询参数,与 `fuzzy_search_result` 二选一 505 | 506 | 返回: 507 | _Response: 响应信息 508 | ''' 509 | 510 | # 构建数据 511 | data = { 512 | 'mainServer': main_server, 513 | 'useEasyBG': settings.use_easy_bg, 514 | 'compress': settings.compress 515 | } 516 | if fuzzy_search_result: 517 | data['fuzzySearchResult'] = fuzzy_search_result 518 | if text: 519 | data['text'] = text 520 | 521 | # 发送请求 522 | return Api( 523 | settings.backend_url, 524 | '/songRandom', 525 | proxy=settings.backend_proxy 526 | ).post(data).json() 527 | 528 | @deprecated("The `ycx` api is now deprecated, use `cutoff_detail` instead.", category=None) 529 | def ycx(server: Server, tier: int, event_id: Optional[int] = None) -> _Response: 530 | '''查询排行榜预测线 531 | 532 | 参数: 533 | server (_Server): 服务器 534 | tier (int): 排行榜挡位 535 | event_id (int): 活动 ID 536 | 537 | 返回: 538 | _Response: 响应信息 539 | ''' 540 | warnings.warn( 541 | "The `ycx` api is now deprecated, use `cutoff_detail` instead.", 542 | DeprecationWarning 543 | ) 544 | 545 | if not isinstance(server, int): 546 | raise ValueError("'server' must be an integer.") 547 | 548 | return cutoff_detail(server, tier, event_id) 549 | 550 | @deprecated("The `ycx_all` api is now deprecated, use `cutoff_all` instead.", category=None) 551 | def ycx_all(server: Server, event_id: Optional[int] = None) -> _Response: 552 | '''查询全挡位预测线 553 | 554 | 参数: 555 | server (_Server): 服务器 556 | event_id (int): 活动 ID 557 | 558 | 返回: 559 | _Response: 响应信息 560 | ''' 561 | warnings.warn( 562 | "The `ycx_all` api is now deprecated, use `cutoff_all` instead.", 563 | DeprecationWarning 564 | ) 565 | 566 | if not isinstance(server, int): 567 | raise ValueError("'server' must be an integer.") 568 | 569 | return cutoff_all(server, event_id) 570 | -------------------------------------------------------------------------------- /tsugu_api_async/_tsugu.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | from typing_extensions import deprecated 3 | from typing import List, Optional, Sequence 4 | 5 | from tsugu_api_core._network import Api 6 | from tsugu_api_core._settings import settings 7 | from tsugu_api_core._typing import ( 8 | _Room, 9 | Server, 10 | ServerId, 11 | _Response, 12 | _DifficultyId, 13 | FuzzySearchResult, 14 | _FuzzySearchResponse 15 | ) 16 | 17 | async def cutoff_all(main_server: ServerId, event_id: Optional[int] = None) -> _Response: 18 | '''查询活动排行榜全部预测线 19 | 20 | 参数: 21 | main_server (_ServerId): 主服务器 22 | event_id (int): 活动 ID 23 | 24 | 返回: 25 | _Response: 响应信息 26 | ''' 27 | 28 | # 构建数据 29 | data = { 30 | 'mainServer': main_server, 31 | 'compress': settings.compress 32 | } 33 | if event_id: 34 | data['eventId'] = event_id 35 | 36 | # 发送请求 37 | return (await Api( 38 | settings.backend_url, 39 | '/cutoffAll', 40 | proxy=settings.backend_proxy 41 | ).apost(data)).json() 42 | 43 | async def cutoff_detail(main_server: ServerId, tier: int, event_id: Optional[int] = None) -> _Response: 44 | '''查询活动排行榜预测线 45 | 46 | 参数: 47 | main_server (_ServerId): 主服务器 48 | tier (int): 排行榜挡位 49 | event_id (int): 活动 ID 50 | 51 | 返回: 52 | _Response: 响应信息 53 | ''' 54 | 55 | # 构建数据 56 | data = { 57 | 'mainServer': main_server, 58 | 'tier': tier, 59 | 'compress': settings.compress 60 | } 61 | if event_id: 62 | data['eventId'] = event_id 63 | 64 | # 发送请求 65 | return (await Api( 66 | settings.backend_url, 67 | '/cutoffDetail', 68 | proxy=settings.backend_proxy 69 | ).apost(data)).json() 70 | 71 | async def cutoff_list_of_recent_event(main_server: ServerId, tier: int, event_id: Optional[int] = None) -> _Response: 72 | '''查询历史活动排行榜预测线 73 | 74 | 参数: 75 | main_server (_ServerId): 主服务器 76 | tier (int): 排行榜挡位 77 | event_id (int): 活动 ID 78 | 79 | 返回: 80 | _Response: 响应信息 81 | ''' 82 | 83 | # 构建数据 84 | data = { 85 | 'mainServer': main_server, 86 | 'tier': tier, 87 | 'compress': settings.compress 88 | } 89 | if event_id: 90 | data['eventId'] = event_id 91 | 92 | # 发送请求 93 | return (await Api( 94 | settings.backend_url, 95 | '/cutoffListOfRecentEvent', 96 | proxy=settings.backend_proxy 97 | ).apost(data)).json() 98 | 99 | async def event_stage(main_server: ServerId, event_id: Optional[int] = None, meta: bool = False) -> _Response: 100 | '''查询团队 LIVE 佳节活动舞台数据 101 | 102 | 参数: 103 | main_server (_ServerId): 主服务器 104 | event_id (int): 活动 ID 105 | meta (bool): 是否携带歌曲分数表 106 | 107 | 返回: 108 | _Response: 响应信息 109 | ''' 110 | 111 | # 构建数据 112 | data = { 113 | 'mainServer': main_server, 114 | 'meta': meta, 115 | 'compress': settings.compress 116 | } 117 | if event_id: 118 | data['eventId'] = event_id 119 | 120 | # 发送请求 121 | return (await Api( 122 | settings.backend_url, 123 | '/eventStage', 124 | proxy=settings.backend_proxy 125 | ).apost(data)).json() 126 | 127 | async def fuzzy_search(text: str) -> _FuzzySearchResponse: 128 | '''模糊搜索 129 | 130 | 参数: 131 | text (str): 搜索文本 132 | 133 | 返回: 134 | _FuzzySearchResponse: 响应信息 135 | ''' 136 | 137 | # 构建数据 138 | data = { 139 | 'text': text, 140 | 'compress': settings.compress 141 | } 142 | 143 | # 发送请求 144 | return (await Api( 145 | settings.backend_url, 146 | '/fuzzySearch', 147 | proxy=settings.backend_proxy 148 | ).apost(data)).json() 149 | 150 | async def gacha_simulate(main_server: ServerId, times: Optional[int] = None, gacha_id: Optional[int] = None) -> _Response: 151 | '''模拟抽卡 152 | 153 | 参数: 154 | main_server (_ServerId): 主服务器 155 | times (int): 抽卡次数 156 | gacha_id (int): 卡池 ID 157 | 158 | 返回: 159 | _Response: 响应信息 160 | ''' 161 | 162 | # 构建数据 163 | data = { 164 | 'mainServer': main_server, 165 | 'compress': settings.compress 166 | } 167 | if times: 168 | data['times'] = times 169 | if gacha_id: 170 | data['gachaId'] = gacha_id 171 | 172 | # 发送请求 173 | return (await Api( 174 | settings.backend_url, 175 | '/gachaSimulate', 176 | proxy=settings.backend_proxy 177 | ).apost(data)).json() 178 | 179 | async def get_card_illustration(card_id: int) -> _Response: 180 | '''获取卡面 181 | 182 | 参数: 183 | card_id (int): 卡片 ID 184 | 185 | 返回: 186 | _Response: 响应信息 187 | ''' 188 | 189 | # 构建数据 190 | data = { 191 | 'cardId': str(card_id) 192 | } 193 | 194 | # 发送请求 195 | return (await Api( 196 | settings.backend_url, 197 | '/getCardIllustration', 198 | proxy=settings.backend_proxy 199 | ).apost(data)).json() 200 | 201 | @deprecated("The `lsycx` api is now deprecated, use `cutoff_list_of_recent_event` instead.", category=None) 202 | async def lsycx(server: Server, tier: int, event_id: Optional[int] = None) -> _Response: 203 | '''查询历史排行榜预测线 204 | 205 | 参数: 206 | server (_Server): 服务器 207 | tier (int): 排行榜挡位 208 | event_id (int): 活动 ID 209 | 210 | 返回: 211 | _Response: 响应信息 212 | ''' 213 | warnings.warn( 214 | "The `lsycx` api is now deprecated, use `cutoff_list_of_recent_event` instead.", 215 | DeprecationWarning 216 | ) 217 | 218 | if not isinstance(server, int): 219 | raise ValueError("'server' must be an integer.") 220 | 221 | return await cutoff_list_of_recent_event(server, tier, event_id) 222 | 223 | async def room_list(room_list: List[_Room]) -> _Response: 224 | '''绘制车牌绘图 225 | 226 | 参数: 227 | room_list (list[_CarData]): 车牌信息列表 228 | 229 | 返回: 230 | _Response: 响应信息 231 | ''' 232 | 233 | # 构建数据 234 | data = { 235 | 'roomList': room_list, 236 | 'compress': settings.compress 237 | } 238 | 239 | # 发送请求 240 | return (await Api( 241 | settings.backend_url, 242 | '/roomList', 243 | proxy=settings.backend_proxy 244 | ).apost(data)).json() 245 | 246 | async def search_card( 247 | displayed_server_list: Sequence[ServerId], 248 | text: Optional[str] = None, 249 | fuzzy_search_result: Optional[FuzzySearchResult] = None 250 | ) -> _Response: 251 | '''查询卡片 252 | 253 | 参数: 254 | displayed_server_list (Sequence[_ServerId]): 展示服务器 255 | text (str): 查询参数,与 `fuzzy_search_result` 二选一 256 | fuzzy_search_result (_FuzzySearchResult): 模糊搜索结果,与 `text` 二选一 257 | 258 | 返回: 259 | _Response: 响应信息 260 | ''' 261 | 262 | # 构建数据 263 | data = { 264 | 'displayedServerList': displayed_server_list, 265 | 'useEasyBG': settings.use_easy_bg, 266 | 'compress': settings.compress 267 | } 268 | if text: 269 | data['text'] = text 270 | if fuzzy_search_result: 271 | data['fuzzySearchResult'] = fuzzy_search_result 272 | 273 | # 发送请求 274 | return (await Api( 275 | settings.backend_url, 276 | '/searchCard', 277 | proxy=settings.backend_proxy 278 | ).apost(data)).json() 279 | 280 | async def search_character( 281 | displayed_server_list: Sequence[ServerId], 282 | fuzzy_search_result: Optional[FuzzySearchResult] = None, 283 | text: Optional[str] = None 284 | ) -> _Response: 285 | '''查询角色 286 | 287 | 参数: 288 | displayed_server_list (Sequence[_ServerId]): 展示服务器 289 | fuzzy_search_result (_FuzzySearchResult): 模糊搜索结果,与 `text` 二选一 290 | text (str): 查询参数,与 `fuzzy_search_result` 二选一 291 | 292 | 返回: 293 | _Response: 响应信息 294 | ''' 295 | 296 | # 构建数据 297 | data = { 298 | 'displayedServerList': displayed_server_list, 299 | 'compress': settings.compress 300 | } 301 | if fuzzy_search_result: 302 | data['fuzzySearchResult'] = fuzzy_search_result 303 | if text: 304 | data['text'] = text 305 | 306 | # 发送请求 307 | return (await Api( 308 | settings.backend_url, 309 | '/searchCharacter', 310 | proxy=settings.backend_proxy 311 | ).apost(data)).json() 312 | 313 | async def search_event( 314 | displayed_server_list: Sequence[ServerId], 315 | fuzzy_search_result: Optional[FuzzySearchResult] = None, 316 | text: Optional[str] = None 317 | ) -> _Response: 318 | '''查询活动 319 | 320 | 参数: 321 | displayed_server_list (Sequence[_ServerId]): 展示服务器 322 | fuzzy_search_result (_FuzzySearchResult): 模糊搜索结果,与 `text` 二选一 323 | text (str): 查询参数,与 `fuzzy_search_result` 二选一 324 | 325 | 返回: 326 | _Response: 响应信息 327 | ''' 328 | 329 | # 构建数据 330 | data = { 331 | 'displayedServerList': displayed_server_list, 332 | 'useEasyBG': settings.use_easy_bg, 333 | 'compress': settings.compress 334 | } 335 | if fuzzy_search_result: 336 | data['fuzzySearchResult'] = fuzzy_search_result 337 | if text: 338 | data['text'] = text 339 | 340 | # 发送请求 341 | return (await Api( 342 | settings.backend_url, 343 | '/searchEvent', 344 | proxy=settings.backend_proxy 345 | ).apost(data)).json() 346 | 347 | async def search_gacha( 348 | displayed_server_list: Sequence[ServerId], 349 | gacha_id: int 350 | ) -> _Response: 351 | '''查询卡池 352 | 353 | 参数: 354 | displayed_server_list (Sequence[_ServerId]): 展示服务器 355 | gacha_id (int): 卡池 ID 356 | 357 | 返回: 358 | _Response: 响应信息 359 | ''' 360 | 361 | # 构建数据 362 | data = { 363 | 'displayedServerList': displayed_server_list, 364 | 'gachaId': gacha_id, 365 | 'useEasyBG': settings.use_easy_bg, 366 | 'compress': settings.compress 367 | } 368 | 369 | # 发送请求 370 | return (await Api( 371 | settings.backend_url, 372 | '/searchGacha', 373 | proxy=settings.backend_proxy 374 | ).apost(data)).json() 375 | 376 | async def search_player(player_id: int, main_server: ServerId) -> _Response: 377 | '''查询玩家状态 378 | 379 | 参数: 380 | player_id (int): 玩家 ID 381 | main_server (_ServerId): 服务器 382 | 383 | 返回: 384 | _Response: 响应信息 385 | ''' 386 | 387 | # 构建数据 388 | data = { 389 | 'playerId': player_id, 390 | 'mainServer': main_server, 391 | 'useEasyBG': settings.use_easy_bg, 392 | 'compress': settings.compress 393 | } 394 | 395 | # 发送请求 396 | return (await Api( 397 | settings.backend_url, 398 | '/searchPlayer', 399 | proxy=settings.backend_proxy 400 | ).apost(data)).json() 401 | 402 | async def search_song( 403 | displayed_server_list: Sequence[ServerId], 404 | fuzzy_search_result: Optional[FuzzySearchResult] = None, 405 | text: Optional[str] = None 406 | ) -> _Response: 407 | '''查询歌曲 408 | 409 | 参数: 410 | displayed_server_list (Sequence[_ServerId]): 展示服务器 411 | fuzzy_search_result (_FuzzySearchResult): 模糊搜索结果,与 `text` 二选一 412 | text (str): 查询参数,与 `fuzzy_search_result` 二选一 413 | 414 | 返回: 415 | _Response: 响应信息 416 | ''' 417 | 418 | # 构建数据 419 | data = { 420 | 'displayedServerList': displayed_server_list, 421 | 'compress': settings.compress 422 | } 423 | if fuzzy_search_result: 424 | data['fuzzySearchResult'] = fuzzy_search_result 425 | if text: 426 | data['text'] = text 427 | 428 | # 发送请求 429 | return (await Api( 430 | settings.backend_url, 431 | '/searchSong', 432 | proxy=settings.backend_proxy 433 | ).apost(data)).json() 434 | 435 | async def song_chart( 436 | displayed_server_list: Sequence[ServerId], 437 | song_id: int, 438 | difficulty_id: _DifficultyId 439 | ) -> _Response: 440 | '''查询歌曲谱面 441 | 442 | 参数: 443 | displayed_server_list (Sequence[_ServerId]): 展示服务器 444 | song_id (int): 歌曲 ID 445 | difficulty_id (_DifficultyId): 难度 ID 446 | 447 | 返回: 448 | _Response: 响应信息 449 | ''' 450 | 451 | # 构建数据 452 | data = { 453 | 'displayedServerList': displayed_server_list, 454 | 'songId': song_id, 455 | 'difficultyId': difficulty_id, 456 | 'compress': settings.compress 457 | } 458 | 459 | # 发送请求 460 | return (await Api( 461 | settings.backend_url, 462 | '/songChart', 463 | proxy=settings.backend_proxy 464 | ).apost(data)).json() 465 | 466 | async def song_meta( 467 | displayed_server_list: Sequence[ServerId], 468 | main_server: ServerId 469 | ) -> _Response: 470 | '''查询歌曲分数表 471 | 472 | 参数: 473 | displayed_server_list (Sequence[_ServerId]): 展示服务器 474 | main_server (_ServerId): 主服务器 475 | 476 | 返回: 477 | _Response: 响应信息 478 | ''' 479 | 480 | # 构建数据 481 | data = { 482 | 'displayedServerList': displayed_server_list, 483 | 'mainServer': main_server, 484 | 'compress': settings.compress 485 | } 486 | 487 | # 发送请求 488 | return (await Api( 489 | settings.backend_url, 490 | '/songMeta', 491 | proxy=settings.backend_proxy 492 | ).apost(data)).json() 493 | 494 | async def song_random( 495 | main_server: ServerId, 496 | fuzzy_search_result: Optional[FuzzySearchResult] = None, 497 | text: Optional[str] = None 498 | ) -> _Response: 499 | '''随机歌曲 500 | 501 | 参数: 502 | main_server (_ServerId): 主服务器 503 | fuzzy_search_result (_FuzzySearchResult): 模糊搜索结果,与 `text` 二选一 504 | text (str): 查询参数,与 `fuzzy_search_result` 二选一 505 | 506 | 返回: 507 | _Response: 响应信息 508 | ''' 509 | 510 | # 构建数据 511 | data = { 512 | 'mainServer': main_server, 513 | 'useEasyBG': settings.use_easy_bg, 514 | 'compress': settings.compress 515 | } 516 | if fuzzy_search_result: 517 | data['fuzzySearchResult'] = fuzzy_search_result 518 | if text: 519 | data['text'] = text 520 | 521 | # 发送请求 522 | return (await Api( 523 | settings.backend_url, 524 | '/songRandom', 525 | proxy=settings.backend_proxy 526 | ).apost(data)).json() 527 | 528 | @deprecated("The `ycx` api is now deprecated, use `cutoff_detail` instead.", category=None) 529 | async def ycx(server: Server, tier: int, event_id: Optional[int] = None) -> _Response: 530 | '''查询排行榜预测线 531 | 532 | 参数: 533 | server (_Server): 服务器 534 | tier (int): 排行榜挡位 535 | event_id (int): 活动 ID 536 | 537 | 返回: 538 | _Response: 响应信息 539 | ''' 540 | warnings.warn( 541 | "The `ycx` api is now deprecated, use `cutoff_detail` instead.", 542 | DeprecationWarning 543 | ) 544 | 545 | if not isinstance(server, int): 546 | raise ValueError("'server' must be an integer.") 547 | 548 | return await cutoff_detail(server, tier, event_id) 549 | 550 | @deprecated("The `ycx_all` api is now deprecated, use `cutoff_all` instead.", category=None) 551 | async def ycx_all(server: Server, event_id: Optional[int] = None) -> _Response: 552 | '''查询全挡位预测线 553 | 554 | 参数: 555 | server (_Server): 服务器 556 | event_id (int): 活动 ID 557 | 558 | 返回: 559 | _Response: 响应信息 560 | ''' 561 | warnings.warn( 562 | "The `ycx_all` api is now deprecated, use `cutoff_all` instead.", 563 | DeprecationWarning 564 | ) 565 | 566 | if not isinstance(server, int): 567 | raise ValueError("'server' must be an integer.") 568 | 569 | return await cutoff_all(server, event_id) 570 | --------------------------------------------------------------------------------