"""EA 系取数(api.gametools.network)。 - `fetch_all_stats`:bf3/bf4/bf1/bfv/bf6 的 `/all` 战绩接口(by-id 与 by-name 并发请求,谁先拿到有效数据用谁)。 - `get_player_info_by_name` / `get_player_info_by_ea_id`:`bfglobal/player` 查人, 绑定流程用。 - `get_bf6_rank`:`bf6/profile` 取等级(playerCard.rank)。 gametools 是同步 requests 的部分一律 `asyncio.to_thread`,避免卡事件循环。 """ import asyncio import json import httpx import requests from nonebot import logger _API = "https://api.gametools.network" _HEADERS = {"accept": "application/json"} async def fetch_all_stats(game, player, player_id, user_id, platform): """查 `{game}/all/`:by-id 与 by-name 并发,先成功(且无 errors)的胜出。""" base = f"{_API}/{game}/all/?format_values=true" url_by_id = ( f"{base}&playerid={player_id}&oid={user_id}" f"&platform={platform}&skip_battlelog=false&lang=en-us" ) url_by_name = ( f"{base}&name={player}&platform={platform}&skip_battlelog=false&lang=en-us" ) async with httpx.AsyncClient(timeout=10.0) as client: # 创建两个并发任务 task_id = asyncio.create_task(client.get(url_by_id)) task_name = asyncio.create_task(client.get(url_by_name)) # 等待第一个完成的任务 done, pending = await asyncio.wait( [task_id, task_name], return_when=asyncio.FIRST_COMPLETED ) # 取消未完成的任务 for task in pending: task.cancel() # 检查已完成的任务是否成功且无 errors for task in done: try: response = await task if response.status_code == 200: data = response.json() if "errors" not in data: logger.info( f"成功获取数据({'ID' if task is task_id else 'Name'}方式)" ) return data except (httpx.RequestError, ValueError) as e: logger.warning(f"请求或解析失败: {e}") # 如果所有尝试都失败,返回错误 return {"errors": ["未找到玩家"]} async def _get_player_json(url: str) -> dict: """requests 放线程池 + 超时;网络/解析失败统一降级成 {"errors": [...]}。""" try: response = await asyncio.to_thread( requests.get, url, headers=_HEADERS, timeout=10 ) return json.loads(response.text) except Exception as err: # noqa: BLE001 - 调用方按 errors 分支处理 logger.warning(f"查询玩家信息失败: {err}") return {"errors": [str(err)]} def player_display_name(info: dict, fallback: str = "error") -> str: """从 bfglobal/player 响应里取展示名。 该接口实测有两种形态:一种给 personaName,另一种(同一玩家也会出现)只给 name + nucleusId;漏掉 name 时卡片会把玩家名显示成 "error"(老代码的问题)。 """ return ( info.get("personaName") or info.get("name") or info.get("displayName") or info.get("username") or fallback ) async def get_player_info_by_name(user_name, platform) -> dict: url = ( f"{_API}/bfglobal/player/?name={user_name}" f"&platform={platform}&skip_battlelog=false" ) logger.info(f"查询用户请求url:{url}") return await _get_player_json(url) async def get_player_info_by_ea_id(player_id, user_id, platform) -> dict: url = ( f"{_API}/bfglobal/player/?playerid={player_id}&oid={user_id}" f"&platform={platform}&skip_battlelog=false" ) return await _get_player_json(url) async def get_bf6_rank(player, player_id, user_id, platform) -> int: """取BF6等级:gametools profile 接口返回 playerProfiles[0].playerCard.rank""" url = ( f"{_API}/bf6/profile/?name={player}" f"&playerid={player_id}&nucleus_id={user_id}&platform={platform}" f"&skip_battlelog=true&lang=en-us" ) try: response = await asyncio.to_thread( requests.get, url, headers=_HEADERS, timeout=10 ) profiles = response.json().get("playerProfiles") or [] except Exception as err: # noqa: BLE001 - 等级取不到就当 0,不打断整卡 logger.warning(f"查询BF6等级失败: {err}") return 0 logger.info(f"BF6 profile 返回:{str(profiles)[:300]}") if not isinstance(profiles, list) or not profiles: logger.info(f"未搜索到用户: {player}") return 0 profile = profiles[0] if isinstance(profiles[0], dict) else {} # 该接口的真实字段:playerCard.rank 是等级数字,rankName 是称号名 rank = (profile.get("playerCard") or {}).get("rank") or 0 logger.info(f"BF6等级: {rank}(称号:{profile.get('rankName')})") return rank