2026-06-11 04:19:54 +02:00
|
|
|
import logging
|
|
|
|
|
import sys
|
feat: M2 (part 2) — RSS poller, backfill, enrichment, scheduler
- Free per-channel RSS reader for quota-less fresh-video detection
- Recent-first backfill (configurable: 100 videos / 1 year) plus resumable deep
backfill from the uploads playlist
- Enrichment via videos.list: duration, view/like counts, category, topics,
language, Shorts heuristic and livestream/premiere classification
- Reusable sync runners + APScheduler jobs (rss / enrich / backfill), all
quota-aware with a reserve so backfill never starves fresh enrichment
- Manual triggers: POST /api/sync/{rss,backfill,enrich}
- Exact insert counting via RETURNING with in-batch de-duplication
2026-06-11 01:36:41 +02:00
|
|
|
from contextlib import asynccontextmanager
|
2026-06-11 01:01:37 +02:00
|
|
|
from pathlib import Path
|
|
|
|
|
|
2026-06-11 02:19:47 +02:00
|
|
|
from fastapi import FastAPI, HTTPException
|
2026-06-11 04:19:54 +02:00
|
|
|
|
|
|
|
|
# Make our own loggers (e.g. the scheduler) visible in container logs — uvicorn's
|
|
|
|
|
# logging config otherwise filters out INFO from non-uvicorn loggers.
|
|
|
|
|
_subfeed_logger = logging.getLogger("subfeed")
|
|
|
|
|
if not _subfeed_logger.handlers:
|
|
|
|
|
_handler = logging.StreamHandler(sys.stdout)
|
|
|
|
|
_handler.setFormatter(logging.Formatter("%(levelname)s [%(name)s] %(message)s"))
|
|
|
|
|
_subfeed_logger.addHandler(_handler)
|
|
|
|
|
_subfeed_logger.setLevel(logging.INFO)
|
|
|
|
|
_subfeed_logger.propagate = False
|
2026-06-11 01:01:37 +02:00
|
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
|
|
|
from fastapi.responses import FileResponse
|
|
|
|
|
from fastapi.staticfiles import StaticFiles
|
|
|
|
|
from starlette.middleware.sessions import SessionMiddleware
|
|
|
|
|
|
|
|
|
|
from app import auth
|
|
|
|
|
from app.config import settings
|
2026-06-11 02:11:02 +02:00
|
|
|
from app.routes import feed, health, me, sync, tags
|
feat: M2 (part 2) — RSS poller, backfill, enrichment, scheduler
- Free per-channel RSS reader for quota-less fresh-video detection
- Recent-first backfill (configurable: 100 videos / 1 year) plus resumable deep
backfill from the uploads playlist
- Enrichment via videos.list: duration, view/like counts, category, topics,
language, Shorts heuristic and livestream/premiere classification
- Reusable sync runners + APScheduler jobs (rss / enrich / backfill), all
quota-aware with a reserve so backfill never starves fresh enrichment
- Manual triggers: POST /api/sync/{rss,backfill,enrich}
- Exact insert counting via RETURNING with in-batch de-duplication
2026-06-11 01:36:41 +02:00
|
|
|
from app.scheduler import shutdown_scheduler, start_scheduler
|
2026-06-11 01:01:37 +02:00
|
|
|
|
feat: M2 (part 2) — RSS poller, backfill, enrichment, scheduler
- Free per-channel RSS reader for quota-less fresh-video detection
- Recent-first backfill (configurable: 100 videos / 1 year) plus resumable deep
backfill from the uploads playlist
- Enrichment via videos.list: duration, view/like counts, category, topics,
language, Shorts heuristic and livestream/premiere classification
- Reusable sync runners + APScheduler jobs (rss / enrich / backfill), all
quota-aware with a reserve so backfill never starves fresh enrichment
- Manual triggers: POST /api/sync/{rss,backfill,enrich}
- Exact insert counting via RETURNING with in-batch de-duplication
2026-06-11 01:36:41 +02:00
|
|
|
|
|
|
|
|
@asynccontextmanager
|
|
|
|
|
async def lifespan(app: FastAPI):
|
|
|
|
|
start_scheduler()
|
|
|
|
|
try:
|
|
|
|
|
yield
|
|
|
|
|
finally:
|
|
|
|
|
shutdown_scheduler()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app = FastAPI(title=settings.app_name, lifespan=lifespan)
|
2026-06-11 01:01:37 +02:00
|
|
|
|
|
|
|
|
app.add_middleware(
|
|
|
|
|
SessionMiddleware,
|
|
|
|
|
secret_key=settings.secret_key,
|
|
|
|
|
same_site="lax",
|
|
|
|
|
https_only=False,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if settings.frontend_origin:
|
|
|
|
|
app.add_middleware(
|
|
|
|
|
CORSMiddleware,
|
|
|
|
|
allow_origins=[settings.frontend_origin],
|
|
|
|
|
allow_credentials=True,
|
|
|
|
|
allow_methods=["*"],
|
|
|
|
|
allow_headers=["*"],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
app.include_router(health.router)
|
|
|
|
|
app.include_router(auth.router)
|
2026-06-11 01:22:07 +02:00
|
|
|
app.include_router(sync.router)
|
2026-06-11 01:57:19 +02:00
|
|
|
app.include_router(tags.router)
|
2026-06-11 02:11:02 +02:00
|
|
|
app.include_router(feed.router)
|
|
|
|
|
app.include_router(me.router)
|
2026-06-11 01:01:37 +02:00
|
|
|
|
2026-06-11 02:19:47 +02:00
|
|
|
# The built SPA (populated by the Docker frontend build stage).
|
|
|
|
|
STATIC_DIR = Path(__file__).parent / "static_spa"
|
|
|
|
|
app.mount(
|
|
|
|
|
"/assets",
|
|
|
|
|
StaticFiles(directory=STATIC_DIR / "assets", check_dir=False),
|
|
|
|
|
name="assets",
|
|
|
|
|
)
|
2026-06-11 01:01:37 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.get("/")
|
|
|
|
|
async def index() -> FileResponse:
|
|
|
|
|
return FileResponse(STATIC_DIR / "index.html")
|
2026-06-11 02:19:47 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.get("/{full_path:path}")
|
|
|
|
|
async def spa_fallback(full_path: str) -> FileResponse:
|
|
|
|
|
# Client-side routes fall back to index.html; real API/asset paths are matched above.
|
|
|
|
|
if full_path.startswith(("api/", "auth/", "healthz", "assets/")):
|
|
|
|
|
raise HTTPException(status_code=404)
|
|
|
|
|
return FileResponse(STATIC_DIR / "index.html")
|