RSS → ntfy bridge with modern web UI
build-and-push / docker (push) Has been cancelled

Features: feed CRUD, per-feed ntfy target (incl. private servers),
Telegram/webhook channels, keyword filters, image attachments,
per-feed intervals, OPML import/export, notification history & stats,
users with roles, admin alerts, RU/EN i18n, light/dark theme,
notification preview, history search, activity chart. Dockerized.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
dimon
2026-06-02 21:11:57 +08:00
commit bf52bc3079
28 changed files with 3396 additions and 0 deletions
+47
View File
@@ -0,0 +1,47 @@
"""APScheduler wrapper that ticks every minute and lets the checker decide
which feeds are due (per-feed intervals are evaluated in check_all_feeds)."""
from __future__ import annotations
import logging
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from apscheduler.triggers.interval import IntervalTrigger
from .checker import check_all_feeds
log = logging.getLogger("scheduler")
_scheduler: AsyncIOScheduler | None = None
_JOB_ID = "check-feeds"
# Fixed tick; per-feed/global intervals are honoured inside check_all_feeds.
_TICK_SECONDS = 60
def start(interval_minutes: int) -> None:
global _scheduler
if _scheduler is not None:
return
_scheduler = AsyncIOScheduler(timezone="UTC")
_scheduler.add_job(
check_all_feeds,
trigger=IntervalTrigger(seconds=_TICK_SECONDS),
id=_JOB_ID,
max_instances=1,
coalesce=True,
replace_existing=True,
)
_scheduler.start()
log.info("Планировщик запущен (тик 60с), интервал по умолчанию %d мин", interval_minutes)
def reschedule(interval_minutes: int) -> None:
# The global interval is read live by the checker each tick, so there is
# nothing to reschedule — kept for API compatibility.
log.info("Интервал по умолчанию изменён на %d мин", interval_minutes)
def shutdown() -> None:
global _scheduler
if _scheduler is not None:
_scheduler.shutdown(wait=False)
_scheduler = None