from __future__ import annotations import asyncio import time from typing import Any, Awaitable, Callable, Optional from fastapi import APIRouter, Query from fastapi.responses import JSONResponse from web2_ip_profile import run_ip_profile from web2_ip_quality import run_ip_quality from web2_network import run_network_dashboard, run_network_toolkit router = APIRouter() _PROFILE_TTL_SECONDS = 300.0 _QUALITY_TTL_SECONDS = 600.0 _NETWORK_TTL_SECONDS = 300.0 _NETWORK_TOOLKIT_TTL_SECONDS = 300.0 _cache_lock = asyncio.Lock() _profile_cache: dict[str, tuple[float, dict[str, Any]]] = {} _quality_cache: tuple[float, dict[str, Any]] | None = None _network_cache: tuple[float, dict[str, Any]] | None = None _network_toolkit_cache: tuple[float, dict[str, Any]] | None = None _inflight: dict[str, asyncio.Task[dict[str, Any]]] = {} def _profile_cache_key(tz: str, lang: str, leak_flag: Optional[bool]) -> str: leak = "none" if leak_flag is None else ("1" if leak_flag else "0") return f"{tz}|{lang}|{leak}" async def _cached_fetch( key: str, *, ttl: float, force_refresh: bool, fetcher: Callable[[], Awaitable[dict[str, Any]]], store: Callable[[str, float, dict[str, Any]], None], load: Callable[[str], Optional[tuple[float, dict[str, Any]]]], ) -> dict[str, Any]: now = time.time() if not force_refresh: cached = load(key) if cached and (now - cached[0]) < ttl: return cached[1] async with _cache_lock: if not force_refresh: cached = load(key) if cached and (time.time() - cached[0]) < ttl: return cached[1] inflight = _inflight.get(key) if inflight is not None: return await asyncio.shield(inflight) task = asyncio.create_task(fetcher()) _inflight[key] = task try: payload = await task finally: async with _cache_lock: if _inflight.get(key) is task: _inflight.pop(key, None) store(key, time.time(), payload) return payload def _store_profile(key: str, at: float, payload: dict[str, Any]) -> None: _profile_cache[key] = (at, payload) def _load_profile(key: str) -> Optional[tuple[float, dict[str, Any]]]: return _profile_cache.get(key) def _store_quality(key: str, at: float, payload: dict[str, Any]) -> None: global _quality_cache _quality_cache = (at, payload) def _load_quality(_key: str) -> Optional[tuple[float, dict[str, Any]]]: return _quality_cache def _store_network(_key: str, at: float, payload: dict[str, Any]) -> None: global _network_cache _network_cache = (at, payload) def _load_network(_key: str) -> Optional[tuple[float, dict[str, Any]]]: return _network_cache def _store_network_toolkit(_key: str, at: float, payload: dict[str, Any]) -> None: global _network_toolkit_cache _network_toolkit_cache = (at, payload) def _load_network_toolkit(_key: str) -> Optional[tuple[float, dict[str, Any]]]: return _network_toolkit_cache async def _get_ip_profile_cached( *, tz: str, lang: str, leak_flag: Optional[bool], force_refresh: bool, ) -> dict[str, Any]: cache_key = _profile_cache_key(tz, lang, leak_flag) return await _cached_fetch( cache_key, ttl=_PROFILE_TTL_SECONDS, force_refresh=force_refresh, fetcher=lambda: run_ip_profile(client_tz=tz, client_lang=lang, webrtc_leak=leak_flag), store=_store_profile, load=_load_profile, ) async def _get_ip_quality_cached(*, force_refresh: bool) -> dict[str, Any]: return await _cached_fetch( "quality", ttl=_QUALITY_TTL_SECONDS, force_refresh=force_refresh, fetcher=run_ip_quality, store=_store_quality, load=_load_quality, ) async def _get_network_status_cached(*, force_refresh: bool) -> dict[str, Any]: return await _cached_fetch( "network_v2", ttl=_NETWORK_TTL_SECONDS, force_refresh=force_refresh, fetcher=run_network_dashboard, store=_store_network, load=_load_network, ) async def _get_network_toolkit_cached(*, force_refresh: bool) -> dict[str, Any]: return await _cached_fetch( "network_toolkit_v2", ttl=_NETWORK_TOOLKIT_TTL_SECONDS, force_refresh=force_refresh, fetcher=run_network_toolkit, store=_store_network_toolkit, load=_load_network_toolkit, ) @router.get("/network_status") async def network_status( force_refresh: bool = Query(False, description="是否跳过缓存强制重新检测"), ): """Google / 抖音 连通与简易网速(供仪表盘)。""" try: return await _get_network_status_cached(force_refresh=force_refresh) except Exception as e: return JSONResponse({"error": str(e)}, status_code=500) @router.get("/network_toolkit") async def network_toolkit( force_refresh: bool = Query(False, description="是否跳过缓存强制重新检测"), ): """网络增强工具聚合:speedtest、mtr、ipinfo、system 信息等。""" try: return await _get_network_toolkit_cached(force_refresh=force_refresh) except Exception as e: return JSONResponse({"error": str(e)}, status_code=500) @router.get("/ip_profile") async def ip_profile( tz: str = Query("", description="客户端 IANA 时区,如 Asia/Shanghai"), lang: str = Query("", description="客户端语言,如 zh-CN"), webrtc_leak: Optional[str] = Query(None, description="1/true 表示检测到 WebRTC 本地泄露"), force_refresh: bool = Query(False, description="是否跳过缓存强制重新检测"), ): """静态住宅 IP / 出口画像(参考 whoer、ping0.cc)。""" leak_flag: Optional[bool] = None if webrtc_leak is not None: leak_flag = str(webrtc_leak).strip().lower() in ("1", "true", "yes") try: return await _get_ip_profile_cached( tz=tz.strip(), lang=lang.strip(), leak_flag=leak_flag, force_refresh=force_refresh, ) except Exception as e: return JSONResponse({"error": str(e)}, status_code=500) @router.get("/ip_quality") async def ip_quality( force_refresh: bool = Query(False, description="是否跳过缓存强制重新检测"), ): """IP 质量检测(集成 xykt/IPQuality 数据源与流媒体解锁)。""" try: return await _get_ip_quality_cached(force_refresh=force_refresh) except Exception as e: return JSONResponse({"error": str(e)}, status_code=500)