feat: APScheduler manager with fetch interval and auto-publish
Made-with: Cursor
This commit is contained in:
154
src/scheduler.py
Normal file
154
src/scheduler.py
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from datetime import date, datetime, time, timedelta
|
||||||
|
|
||||||
|
from apscheduler.schedulers.background import BackgroundScheduler
|
||||||
|
from apscheduler.triggers.cron import CronTrigger
|
||||||
|
from apscheduler.triggers.interval import IntervalTrigger
|
||||||
|
|
||||||
|
import config
|
||||||
|
from app import db
|
||||||
|
from src.models import Article, Issue, Setting
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class SchedulerManager:
|
||||||
|
def __init__(self, app):
|
||||||
|
self.app = app
|
||||||
|
self.scheduler = BackgroundScheduler()
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
interval = Setting.get(
|
||||||
|
"fetch_interval_hours", default=config.FETCH_INTERVAL_HOURS
|
||||||
|
)
|
||||||
|
self.scheduler.add_job(
|
||||||
|
self._run_fetch,
|
||||||
|
IntervalTrigger(hours=interval),
|
||||||
|
id="rss_fetch",
|
||||||
|
replace_existing=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
auto_pub = Setting.get("auto_publish", default=None)
|
||||||
|
if auto_pub:
|
||||||
|
self.enable_auto_publish(
|
||||||
|
day_of_week=auto_pub["day_of_week"],
|
||||||
|
hour=auto_pub["hour"],
|
||||||
|
minute=auto_pub["minute"],
|
||||||
|
cover_method=auto_pub["cover_method"],
|
||||||
|
)
|
||||||
|
|
||||||
|
self.scheduler.start()
|
||||||
|
logger.info("Scheduler started")
|
||||||
|
|
||||||
|
def shutdown(self):
|
||||||
|
if self.scheduler.running:
|
||||||
|
self.scheduler.shutdown(wait=False)
|
||||||
|
|
||||||
|
def _run_fetch(self):
|
||||||
|
with self.app.app_context():
|
||||||
|
from src.fetcher import fetch_and_cache_articles
|
||||||
|
|
||||||
|
result = fetch_and_cache_articles()
|
||||||
|
logger.info("Fetch completed: %s", result)
|
||||||
|
|
||||||
|
def _run_auto_publish(self):
|
||||||
|
with self.app.app_context():
|
||||||
|
from src.cover import generate_cover
|
||||||
|
from src.epub_builder import build_epub
|
||||||
|
|
||||||
|
today = date.today()
|
||||||
|
week_start = today - timedelta(days=today.weekday())
|
||||||
|
week_end = week_start + timedelta(days=6)
|
||||||
|
|
||||||
|
week_after = week_end + timedelta(days=1)
|
||||||
|
articles = (
|
||||||
|
Article.query.filter(
|
||||||
|
Article.pub_date >= datetime.combine(week_start, time.min)
|
||||||
|
)
|
||||||
|
.filter(Article.pub_date < datetime.combine(week_after, time.min))
|
||||||
|
.order_by(Article.pub_date.asc())
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
|
||||||
|
if not articles:
|
||||||
|
logger.info("No articles for auto-publish, skipping")
|
||||||
|
return
|
||||||
|
|
||||||
|
article_ids = [a.id for a in articles]
|
||||||
|
headlines = [a.title for a in articles]
|
||||||
|
|
||||||
|
auto_pub = Setting.get("auto_publish", {})
|
||||||
|
method = auto_pub.get("cover_method", "text")
|
||||||
|
|
||||||
|
cover_path = generate_cover(
|
||||||
|
method, config.ISSUES_DIR, week_start, week_end, headlines
|
||||||
|
)
|
||||||
|
epub_path = build_epub(
|
||||||
|
week_start, week_end, article_ids, cover_path, config.ISSUES_DIR
|
||||||
|
)
|
||||||
|
|
||||||
|
issue = Issue(
|
||||||
|
week_start=week_start,
|
||||||
|
week_end=week_end,
|
||||||
|
cover_method=method,
|
||||||
|
cover_path=cover_path,
|
||||||
|
epub_path=epub_path,
|
||||||
|
article_ids=json.dumps(article_ids),
|
||||||
|
excluded_article_ids=json.dumps([]),
|
||||||
|
status="published",
|
||||||
|
)
|
||||||
|
db.session.add(issue)
|
||||||
|
db.session.commit()
|
||||||
|
logger.info("Auto-published issue: %s", epub_path)
|
||||||
|
|
||||||
|
def update_fetch_interval(self, hours: int):
|
||||||
|
Setting.set("fetch_interval_hours", hours)
|
||||||
|
self.scheduler.reschedule_job(
|
||||||
|
"rss_fetch", trigger=IntervalTrigger(hours=hours)
|
||||||
|
)
|
||||||
|
|
||||||
|
def enable_auto_publish(
|
||||||
|
self,
|
||||||
|
day_of_week: str,
|
||||||
|
hour: int,
|
||||||
|
minute: int,
|
||||||
|
cover_method: str,
|
||||||
|
):
|
||||||
|
Setting.set(
|
||||||
|
"auto_publish",
|
||||||
|
{
|
||||||
|
"day_of_week": day_of_week,
|
||||||
|
"hour": hour,
|
||||||
|
"minute": minute,
|
||||||
|
"cover_method": cover_method,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.scheduler.add_job(
|
||||||
|
self._run_auto_publish,
|
||||||
|
CronTrigger(day_of_week=day_of_week, hour=hour, minute=minute),
|
||||||
|
id="auto_publish",
|
||||||
|
replace_existing=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def disable_auto_publish(self):
|
||||||
|
Setting.set("auto_publish", None)
|
||||||
|
try:
|
||||||
|
self.scheduler.remove_job("auto_publish")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_status(self) -> dict:
|
||||||
|
status = {"running": self.scheduler.running}
|
||||||
|
fetch_job = self.scheduler.get_job("rss_fetch")
|
||||||
|
if fetch_job:
|
||||||
|
status["rss_fetch"] = {
|
||||||
|
"next_run": str(fetch_job.next_run_time),
|
||||||
|
"interval_hours": fetch_job.trigger.interval.total_seconds() / 3600,
|
||||||
|
}
|
||||||
|
pub_job = self.scheduler.get_job("auto_publish")
|
||||||
|
if pub_job:
|
||||||
|
status["auto_publish"] = {
|
||||||
|
"next_run": str(pub_job.next_run_time),
|
||||||
|
}
|
||||||
|
return status
|
||||||
55
tests/test_scheduler.py
Normal file
55
tests/test_scheduler.py
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
from src.scheduler import SchedulerManager
|
||||||
|
|
||||||
|
|
||||||
|
def test_scheduler_starts_fetch_job(app):
|
||||||
|
with app.app_context():
|
||||||
|
mgr = SchedulerManager(app)
|
||||||
|
mgr.start()
|
||||||
|
jobs = mgr.scheduler.get_jobs()
|
||||||
|
job_ids = [j.id for j in jobs]
|
||||||
|
assert "rss_fetch" in job_ids
|
||||||
|
mgr.shutdown()
|
||||||
|
|
||||||
|
|
||||||
|
def test_scheduler_update_fetch_interval(app):
|
||||||
|
with app.app_context():
|
||||||
|
mgr = SchedulerManager(app)
|
||||||
|
mgr.start()
|
||||||
|
mgr.update_fetch_interval(2)
|
||||||
|
job = mgr.scheduler.get_job("rss_fetch")
|
||||||
|
assert job is not None
|
||||||
|
assert job.trigger.interval.total_seconds() == 7200
|
||||||
|
mgr.shutdown()
|
||||||
|
|
||||||
|
|
||||||
|
def test_scheduler_enable_auto_publish(app):
|
||||||
|
with app.app_context():
|
||||||
|
mgr = SchedulerManager(app)
|
||||||
|
mgr.start()
|
||||||
|
mgr.enable_auto_publish(day_of_week="sun", hour=6, minute=0,
|
||||||
|
cover_method="text")
|
||||||
|
job = mgr.scheduler.get_job("auto_publish")
|
||||||
|
assert job is not None
|
||||||
|
mgr.shutdown()
|
||||||
|
|
||||||
|
|
||||||
|
def test_scheduler_disable_auto_publish(app):
|
||||||
|
with app.app_context():
|
||||||
|
mgr = SchedulerManager(app)
|
||||||
|
mgr.start()
|
||||||
|
mgr.enable_auto_publish(day_of_week="sun", hour=6, minute=0,
|
||||||
|
cover_method="text")
|
||||||
|
mgr.disable_auto_publish()
|
||||||
|
job = mgr.scheduler.get_job("auto_publish")
|
||||||
|
assert job is None
|
||||||
|
mgr.shutdown()
|
||||||
|
|
||||||
|
|
||||||
|
def test_scheduler_get_status(app):
|
||||||
|
with app.app_context():
|
||||||
|
mgr = SchedulerManager(app)
|
||||||
|
mgr.start()
|
||||||
|
status = mgr.get_status()
|
||||||
|
assert status["running"] is True
|
||||||
|
assert "rss_fetch" in status
|
||||||
|
mgr.shutdown()
|
||||||
Reference in New Issue
Block a user