# --- routers/leaf_options.py --- from fastapi import APIRouter, BackgroundTasks, HTTPException, Body from fastapi.responses import StreamingResponse from pydantic import BaseModel from typing import List import json, os, asyncio, time, re # 🌟 引入新的 asyncssh 版本爬蟲 from cmts_scraper import sync_cmts_leaves_async from shared import CMTS_DEVICE # 🌟 移除原本硬寫的 prefix="/api/v1",只留下 tags router = APIRouter(tags=["Options"]) CACHE_FILE = "cmts_leaf_options_cache.json" # 🌟 1. 改用全域布林變數,狀態切換更即時,消滅非同步時間差 IS_SCANNING = False class SyncOptionsRequest(BaseModel): leaf_paths: List[str] @router.get("/cmts-leaf-options") async def get_leaf_options(): if not os.path.exists(CACHE_FILE): return {} with open(CACHE_FILE, "r", encoding="utf-8") as f: return json.load(f) @router.post("/cmts-leaf-options/sync") async def sync_leaf_options(request: SyncOptionsRequest): global IS_SCANNING # 宣告我們要修改全域變數 if not request.leaf_paths: raise HTTPException(status_code=400) # 🌟 2. 第一時間檢查並「立即」阻擋 if IS_SCANNING: async def busy_stream(): yield json.dumps({"event": "error", "message": "⚠️ 掃描任務正在執行中,請勿重複點擊!"}) + "\n" return StreamingResponse(busy_stream(), media_type="text/event-stream") # 如果沒人掃描,立刻把狀態改為 True (在建立串流之前就上鎖) IS_SCANNING = True async def event_generator(): global IS_SCANNING try: cmts_query_paths = [] for p in request.leaf_paths: clean_p = p.replace("::", " ") clean_p = re.sub(r"\s*\[\d+\]", "", clean_p) cmts_query_paths.append(clean_p) cmts_query_paths = list(dict.fromkeys(cmts_query_paths)) # 🌟 因為爬蟲自己會存檔,我們只要專心把進度轉發給前端就好! async for chunk in sync_cmts_leaves_async( host=CMTS_DEVICE.get("host"), username=CMTS_DEVICE.get("username"), password=CMTS_DEVICE.get("password"), leaf_paths=cmts_query_paths ): yield chunk except Exception as e: yield json.dumps({"event": "error", "message": str(e)}) + "\n" finally: # 🌟 3. 無論成功、失敗、或使用者提早關閉網頁,都確保會解鎖 IS_SCANNING = False return StreamingResponse(event_generator(), media_type="text/event-stream") # 🌟 新增:清除特定選項快取的 API (使用您原本定義的 CACHE_FILE) @router.post("/clear_cache") async def clear_specific_cache(paths_to_clear: list = Body(...)): cleared_count = 0 # 1. 檢查快取檔案是否存在 if not os.path.exists(CACHE_FILE): return {"status": "success", "cleared_count": 0, "message": "快取檔案不存在"} try: # 2. 讀取現有的 JSON 快取資料 with open(CACHE_FILE, "r", encoding="utf-8") as f: cache_data = json.load(f) # 3. 逐一比對並刪除指定的路徑 for path in paths_to_clear: if path in cache_data: del cache_data[path] cleared_count += 1 # 4. 如果有刪除資料,將更新後的內容寫回 JSON 檔案 if cleared_count > 0: with open(CACHE_FILE, "w", encoding="utf-8") as f: json.dump(cache_data, f, ensure_ascii=False, indent=4) return {"status": "success", "cleared_count": cleared_count} except Exception as e: return {"status": "error", "message": f"清除快取時發生錯誤: {str(e)}"}