2026-04-06 15:18:37 -04:00
|
|
|
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]
|
2026-04-06 18:59:51 -04:00
|
|
|
headlines = [
|
|
|
|
|
a.title for a in articles
|
|
|
|
|
if "Obituaries" not in json.loads(a.categories)
|
|
|
|
|
]
|
2026-04-06 15:18:37 -04:00
|
|
|
|
2026-04-06 17:02:36 -04:00
|
|
|
categories_list = []
|
|
|
|
|
for a in articles:
|
|
|
|
|
categories_list.extend(json.loads(a.categories))
|
|
|
|
|
|
2026-04-06 15:18:37 -04:00
|
|
|
auto_pub = Setting.get("auto_publish", {})
|
|
|
|
|
method = auto_pub.get("cover_method", "text")
|
|
|
|
|
|
|
|
|
|
cover_path = generate_cover(
|
2026-04-06 17:02:36 -04:00
|
|
|
method, config.ISSUES_DIR, week_start, week_end,
|
|
|
|
|
headlines, categories_list
|
2026-04-06 15:18:37 -04:00
|
|
|
)
|
|
|
|
|
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
|