feat: auto-refresh calendar subscriptions
ci / gitleaks (push) Successful in 7s
ci / docker (push) Successful in 3m29s

This commit is contained in:
2026-09-21 15:36:09 +08:00
parent a970a03f5d
commit 97c1bd4e87
6 changed files with 257 additions and 42 deletions
+6 -36
View File
@@ -1,4 +1,4 @@
from datetime import UTC, datetime, timedelta
from datetime import datetime, timedelta
from uuid import UUID
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
@@ -9,8 +9,9 @@ from sqlalchemy.ext.asyncio import AsyncSession
from . import calendar as calendar_service
from .auth import current_user
from .calendar_refresh import refresh_subscription_cache
from .db import get_db
from .models import CalendarSubscription, User, utcnow
from .models import CalendarSubscription, User
router = APIRouter(prefix="/api/v1", tags=["calendar"])
MAX_WINDOW = timedelta(days=366)
@@ -90,37 +91,6 @@ async def _owned(db: AsyncSession, user_id: UUID, subscription_id: UUID) -> Cale
return row
async def _refresh(db: AsyncSession, row: CalendarSubscription) -> None:
try:
result = await __import__("asyncio").to_thread(
calendar_service.fetch_calendar, row.url, etag=row.etag, last_modified=row.last_modified
)
if result.not_modified:
if not row.ics_cache:
raise HTTPException(502, "calendar returned not modified without cache")
elif result.content is not None:
# Parse before replacing a known-good cache.
calendar_service.parse_ics_events(
result.content,
row.name,
row.color,
datetime.now(UTC) - timedelta(days=1),
datetime.now(UTC) + timedelta(days=1),
"UTC",
)
row.ics_cache = result.content.decode("utf-8-sig")
row.etag = result.etag
row.last_modified = result.last_modified
row.refreshed_at = utcnow()
row.last_error = None
except Exception as exc:
row.last_error = exc.detail if isinstance(exc, HTTPException) else str(exc)
if not row.ics_cache:
await db.rollback()
raise HTTPException(502, row.last_error) from exc
await db.commit()
@router.get("/calendar-subscriptions", response_model=list[SubscriptionOut])
async def list_subscriptions(user: User = Depends(current_user), db: AsyncSession = Depends(get_db)):
rows = (await db.scalars(select(CalendarSubscription).where(
@@ -139,7 +109,7 @@ async def create_subscription(
row = CalendarSubscription(user_id=user.id, **payload.model_dump())
db.add(row)
await db.flush()
await _refresh(db, row)
await refresh_subscription_cache(db, row)
await db.refresh(row)
return _out(row)
@@ -183,7 +153,7 @@ async def refresh_subscription(
db: AsyncSession = Depends(get_db),
):
row = await _owned(db, user.id, subscription_id)
await _refresh(db, row)
await refresh_subscription_cache(db, row)
await db.refresh(row)
return _out(row)
@@ -209,7 +179,7 @@ async def calendar_events(
sources = []
for row in rows:
if not row.ics_cache:
await _refresh(db, row)
await refresh_subscription_cache(db, row)
try:
parsed = calendar_service.parse_ics_events(
row.ics_cache or "", row.name, row.color, start, end, user.timezone,