Spaces:
Running
Running
| """VNEWS v2 Patch - auto scheduler + status endpoints + keep-alive. | |
| This is imported by app_v2_entry.py to add auto posting functionality. | |
| FIX v2: Catch-up scheduler + keep-alive to prevent Space sleep | |
| """ | |
| import sys, os, threading, json, time, logging | |
| from datetime import datetime, timezone, timedelta | |
| from fastapi import Request | |
| from fastapi.responses import JSONResponse | |
| import requests as _req | |
| VN_TZ = timezone(timedelta(hours=7)) | |
| LOG = logging.getLogger("app_v2_patch") | |
| LOG.setLevel(logging.INFO) | |
| if not LOG.handlers: | |
| ch = logging.StreamHandler() | |
| ch.setFormatter(logging.Formatter('%(asctime)s [app_v2_patch] %(levelname)s: %(message)s')) | |
| LOG.addHandler(ch) | |
| # ===== Keep-alive: prevent Space from sleeping ===== | |
| # HF Spaces sleep after ~30 min of inactivity on free tier | |
| # This thread pings the Space every 10 minutes to keep it alive | |
| SPACE_URL = "https://bep40-vnews.hf.space" | |
| def _keep_alive_loop(): | |
| """Ping the Space every 10 minutes to prevent sleep.""" | |
| LOG.info(f"๐ Keep-alive thread started - ping {SPACE_URL} every 10 min") | |
| while True: | |
| try: | |
| time.sleep(600) # 10 minutes | |
| _req.get(f"{SPACE_URL}/api/scheduler/status", | |
| headers={"User-Agent": "VNEWS-KeepAlive/1.0"}, | |
| timeout=15) | |
| LOG.debug("Keep-alive ping OK") | |
| except Exception as e: | |
| LOG.warning(f"Keep-alive ping failed (Space may be sleeping): {e}") | |
| # Start keep-alive in background | |
| try: | |
| _ka_thread = threading.Thread(target=_keep_alive_loop, daemon=True, name="keep-alive") | |
| _ka_thread.start() | |
| LOG.info("๐ Keep-alive started - Space will stay awake") | |
| except Exception as e: | |
| LOG.warning(f"Keep-alive start failed: {e}") | |
| # ===== Start auto scheduler ===== | |
| try: | |
| import auto_scheduler as _as | |
| _as.start_auto_scheduler() | |
| LOG.info("[auto_scheduler] Started successfully - will post at 7:00, 13:00, 19:00 VN time (with catch-up)") | |
| except Exception as e: | |
| LOG.error(f"[auto_scheduler] Start failed: {e}") | |
| def register_scheduler_endpoints(app): | |
| """Register scheduler status/trigger endpoints on the FastAPI app.""" | |
| def scheduler_status(): | |
| running = any(t.name == 'auto-scheduler' and t.is_alive() for t in threading.enumerate()) | |
| keep_alive = any(t.name == 'keep-alive' and t.is_alive() for t in threading.enumerate()) | |
| # Load state to show which slots ran today | |
| today_str = datetime.now(VN_TZ).strftime('%Y-%m-%d') | |
| state = {} | |
| try: | |
| state_file = '/data/scheduler_state.json' if os.path.isdir('/data') else None | |
| if state_file and os.path.exists(state_file): | |
| state = json.load(open(state_file, 'r')) | |
| except: | |
| pass | |
| ran_today = state.get(today_str, {}) if state else {} | |
| return JSONResponse({ | |
| "running": running, | |
| "keep_alive": keep_alive, | |
| "schedule": "7:00, 13:00, 19:00 VN time", | |
| "today": today_str, | |
| "slots_ran_today": ran_today, | |
| "catch_up_enabled": True, | |
| "next_run": "7:00, 13:00, or 19:00 VN time (whichever is next)" | |
| }) | |
| async def scheduler_trigger(): | |
| try: | |
| import auto_scheduler as _as2 | |
| _as2._run_scheduled_posting() | |
| return JSONResponse({"ok": True, "message": "Scheduled posting triggered manually"}) | |
| except Exception as e: | |
| return JSONResponse({"ok": False, "error": str(e)}, status_code=500) | |
| def scheduler_force(): | |
| """Force-run all missed slots immediately. Useful after deploy.""" | |
| try: | |
| import auto_scheduler as _as2 | |
| _as2._check_missed_slots() | |
| return JSONResponse({"ok": True, "message": "Missed slots check triggered"}) | |
| except Exception as e: | |
| return JSONResponse({"ok": False, "error": str(e)}, status_code=500) | |
| return app | |
| # Auto-register on the main app from app_v2_entry | |
| try: | |
| from main import app | |
| register_scheduler_endpoints(app) | |
| LOG.info("[app_v2_patch] Scheduler endpoints registered: /api/scheduler/status, /api/scheduler/trigger, /api/scheduler/force") | |
| except Exception as e: | |
| LOG.error(f"[app_v2_patch] Could not register endpoints: {e}") | |