Files
pokemon-radar-api/pokemon_radar_api.py
Timmy f7ef105436 init: Redis-backed Pokémon 雷達 API
- radar_fetcher.py:呼叫 Function Server 抓 twpkinfo.com 資料後寫入 Redis (TTL 300s)
- pokemon_radar_api.py:FastAPI /scan,從 Redis 讀取並依 request 即時二次篩選;Redis 不可用時回 200 空結果,不再 500
- pokemon_location_fetcher.py:舊版 CLI,仍由 fetch_and_upload.sh 使用
- 文件:README (參考手冊)、QUICKSTART (操作指南)、SUMMARY (故事線)、CLAUDE.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 09:13:59 +08:00

177 lines
5.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}