import json import os import sys from typing import Any, Dict, List, Optional import redis from dotenv import load_dotenv from fastapi import FastAPI, HTTPException from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel, Field # 引用舊有的 Client 主要是為了使用它的 postprocess 篩選邏輯,不進行連線 from pokemon_location_fetcher import PokemonRadarClient # 載入環境變數 load_dotenv() # ========= 設定 ========= # Redis 連線資訊 (指向 192.168.42.211) REDIS_HOST = os.getenv("REDIS_HOST", "192.168.42.211") REDIS_PORT = int(os.getenv("REDIS_PORT", 6379)) REDIS_DB = int(os.getenv("REDIS_DB", 0)) REDIS_PASSWORD = os.getenv("REDIS_PASSWORD", None) REDIS_KEY = "pokemon:radar:latest" # ========= FastAPI 初始化 ========= app = FastAPI(title="Pokemon Radar API (Redis Backed)", version="2.0.0") app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # ========= Redis 連線池 ========= try: redis_pool = redis.ConnectionPool( host=REDIS_HOST, port=REDIS_PORT, db=REDIS_DB, password=REDIS_PASSWORD, decode_responses=True, socket_connect_timeout=5 ) except Exception as e: print(f"❌ Redis 設定錯誤: {e}") sys.exit(1) # ========= Models (保持與原本相容) ========= class ScanRequest(BaseModel): # 雖然改用 Redis,但保留這些欄位讓前端/n8n 不用改 # 實際上 lat, lng, zoom, shot, state_path, ua... 在此模式下會被忽略 lat: float = Field(..., description="(已忽略) 緯度") lng: float = Field(..., description="(已忽略) 經度") zoom: int = Field(16, description="(已忽略) 地圖縮放") shot: bool = Field(False, description="(已忽略) 是否擷取畫面") state_path: Optional[str] = Field(None, description="(已忽略)") ua_from_state: bool = Field(True, description="(已忽略)") # === 這些篩選參數依然有效 === min_remaining: Optional[int] = Field(None, ge=0, description="只保留 剩餘秒數 ≥ 此值") min_iv: Optional[int] = Field(None, ge=0, le=100, description="只保留 IV% ≥ 此值") perfect_only: bool = Field(False, description="只保留 100% IV") species: Optional[str] = Field("", description="dex id 或關鍵字,例如 '133,伊布'") save_files: bool = Field(False, description="(已忽略) API 不再負責存檔") class Item(BaseModel): id: Optional[int] name: Optional[str] gender: Optional[str] remaining: Optional[int] expire_time: Optional[int] = None expire_at: Optional[str] = None latitude: Optional[float] longitude: Optional[float] iv_pct: Optional[int] iv_atk: Optional[int] iv_def: Optional[int] iv_sta: Optional[int] cp: Optional[int] level: Optional[int] is_perfect: bool fast_move: Optional[str] = None charge_move: Optional[str] = None class ScanResponse(BaseModel): source: str = "redis" ttl: int count_raw: int count_filtered: int items: List[Item] debug: Dict[str, Any] # ========= 工具:Redis Helper ========= def get_data_from_redis(): try: r = redis.Redis(connection_pool=redis_pool) if not r.exists(REDIS_KEY): return None, 0, None ttl = r.ttl(REDIS_KEY) raw_json = r.get(REDIS_KEY) except redis.exceptions.RedisError as e: return None, 0, f"redis unreachable: {e}" try: return json.loads(raw_json), ttl, None except json.JSONDecodeError: return None, 0, "redis payload not json" # ========= 依賴:Client (只用來做 postprocess) ========= # 我們不需要傳入真實的 url/token,因為不會用到 fetch _filter_helper = PokemonRadarClient(url="", token="") # ========= 路由 ========= @app.get("/ping") def ping(): return {"status": "ok", "backend": "redis"} @app.post("/scan", response_model=ScanResponse) def scan(req: ScanRequest) -> Any: # 1. 從 Redis 讀取資料 data, ttl, err = get_data_from_redis() if not data: # Redis 沒資料或連不上,都回傳空結果而不是 500,避免前端炸掉 return { "source": "redis (unreachable)" if err else "redis (empty)", "ttl": 0, "count_raw": 0, "count_filtered": 0, "items": [], "debug": {"note": err or "Redis key not found or expired"} } # 2. 取得原始列表 # 根據你的 twpk_radar.mjs 結構,items 可能在 'results' 或 'items' 欄位 items_raw = data.get("results", data.get("items", [])) # 3. 使用 Helper 進行篩選 (Process & Filter) # 這一步很重要,因為 Redis 存的是 raw data,我們要根據 API 請求即時篩選 try: items_filtered = _filter_helper.postprocess( items_raw, min_remaining=req.min_remaining, min_iv=req.min_iv, perfect_only=req.perfect_only, species=req.species or "", ) except Exception as e: raise HTTPException(status_code=500, detail=f"Filtering error: {e}") # 4. 準備 Debug 資訊 debug_info = data.get("debug", {}) # 可以在這裡加入來源資訊 debug_info["api_source"] = "Cached from Redis" debug_info["redis_ttl"] = ttl # 5. 回傳結果 return { "source": "redis", "ttl": ttl, "count_raw": len(items_raw), "count_filtered": len(items_filtered), "items": items_filtered, "debug": debug_info }