VNEWS / app_v2_patch.py
dev
Restore VNEWS Space + feat: rewrite-image picker, fixed_image, music proxy
ff7aefb
Raw
History Blame Contribute Delete
4.41 kB
"""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."""
@app.get('/api/scheduler/status')
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)"
})
@app.post('/api/scheduler/trigger')
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)
@app.get('/api/scheduler/force')
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}")