Channel features: subscribers, new-videos badges, activity-driven sync
- Store subscriber count from YouTube statistics and show it on channel page - Sync 50 videos per channel with playlistItems pagination support - Show per-channel and per-category new-videos counters (2-day window) - Replace hourly videos sync with activity trigger (2h idle) and incremental backfill (hard cap 200 per channel) - Clicking the sidebar new-videos count filters the category feed to recent videos only (new_only) - Update agent-team docs: deploy after green checks
This commit is contained in:
parent
fde9a439df
commit
e10df8dcbd
27 changed files with 1420 additions and 107 deletions
91
backend/app/services/sync_trigger.py
Normal file
91
backend/app/services/sync_trigger.py
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
"""Activity-triggered videos sync.
|
||||
|
||||
The hourly scheduled videos sync is gone; instead, every authenticated request
|
||||
(require_session) calls maybe_trigger_videos_sync(). The check must stay cheap:
|
||||
if the sync is not running and enough idle time has passed since the last
|
||||
completed sync, a background thread is started which calls sync.sync_videos
|
||||
with its own DB session.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import threading
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from app.config import settings
|
||||
from app.db import SessionLocal
|
||||
from app.services import sync
|
||||
from app.services.state import get_setting
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def is_videos_sync_due(finished_at_iso: str | None, now: datetime, idle_hours: int) -> bool:
|
||||
"""Pure predicate: should an automatic videos sync start now?
|
||||
|
||||
True when the last completed sync's finished_at is missing/unparseable or
|
||||
older than `idle_hours`."""
|
||||
if not finished_at_iso:
|
||||
return True
|
||||
if not isinstance(finished_at_iso, str):
|
||||
return True
|
||||
try:
|
||||
finished_at = datetime.fromisoformat(finished_at_iso.replace("Z", "+00:00"))
|
||||
except (ValueError, TypeError):
|
||||
return True
|
||||
if finished_at.tzinfo is None:
|
||||
finished_at = finished_at.replace(tzinfo=timezone.utc)
|
||||
if now.tzinfo is None:
|
||||
now = now.replace(tzinfo=timezone.utc)
|
||||
return now - finished_at >= timedelta(hours=idle_hours)
|
||||
|
||||
|
||||
def _extract_finished_at(raw: str | None) -> str | None:
|
||||
if not raw:
|
||||
return None
|
||||
try:
|
||||
payload = json.loads(raw)
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
if not isinstance(payload, dict):
|
||||
return None
|
||||
return payload.get("finished_at")
|
||||
|
||||
|
||||
def _run_videos_sync() -> None:
|
||||
db = SessionLocal()
|
||||
try:
|
||||
sync.sync_videos(db)
|
||||
except sync.SyncInProgress:
|
||||
# Lost the race against a manual sync or another trigger: fine, the
|
||||
# other run will record the status.
|
||||
logger.info("Triggered videos sync skipped: already running")
|
||||
except Exception:
|
||||
logger.exception("Triggered videos sync failed")
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
def maybe_trigger_videos_sync() -> None:
|
||||
"""Cheap per-request check; never raises. Starts a background videos sync
|
||||
when due, or does nothing when a sync is already running."""
|
||||
try:
|
||||
if sync.is_videos_sync_running():
|
||||
return
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
raw = get_setting(db, sync.VIDEOS_SYNC_STATUS_KEY)
|
||||
finished_at = _extract_finished_at(raw)
|
||||
if not is_videos_sync_due(finished_at, datetime.now(timezone.utc), settings.videos_sync_idle_hours):
|
||||
return
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
logger.info(
|
||||
"Videos sync due (last finished: %s), starting in background thread", finished_at
|
||||
)
|
||||
threading.Thread(target=_run_videos_sync, daemon=True).start()
|
||||
except Exception:
|
||||
# Never break the request because of the trigger itself.
|
||||
logger.exception("Failed to evaluate videos sync trigger")
|
||||
Loading…
Add table
Add a link
Reference in a new issue