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>
This commit is contained in:
176
pokemon_radar_api.py
Normal file
176
pokemon_radar_api.py
Normal file
@@ -0,0 +1,176 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user