mirror of
https://github.com/xpltdco/media-rip.git
synced 2026-04-03 10:54:00 -06:00
Full-featured self-hosted yt-dlp web frontend:
- Python 3.12+ / FastAPI backend with async SQLite, SSE transport, session isolation
- Vue 3 / TypeScript / Pinia frontend with real-time progress, theme picker
- 3 built-in themes (cyberpunk/dark/light) + drop-in custom theme system
- Admin auth (bcrypt), purge system, cookie upload, file serving
- Docker multi-stage build, GitHub Actions CI/CD
- 179 backend tests, 29 frontend tests (208 total)
Slices: S01 (Foundation), S02 (SSE+Sessions), S03 (Frontend),
S04 (Admin+Auth), S05 (Themes), S06 (Docker+CI)
133 lines
4.8 KiB
Python
133 lines
4.8 KiB
Python
"""media.rip() — FastAPI application entry point.
|
|
|
|
The lifespan context manager wires together config, database, SSE broker,
|
|
download service, and purge scheduler. All services are stored on
|
|
``app.state`` for access from route handlers via ``request.app.state``.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import logging
|
|
from contextlib import asynccontextmanager
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
|
|
from fastapi import FastAPI
|
|
|
|
from app.core.config import AppConfig
|
|
from app.core.database import close_db, init_db
|
|
from app.core.sse_broker import SSEBroker
|
|
from app.middleware.session import SessionMiddleware
|
|
from app.routers.admin import router as admin_router
|
|
from app.routers.cookies import router as cookies_router
|
|
from app.routers.downloads import router as downloads_router
|
|
from app.routers.files import router as files_router
|
|
from app.routers.formats import router as formats_router
|
|
from app.routers.health import router as health_router
|
|
from app.routers.sse import router as sse_router
|
|
from app.routers.system import router as system_router
|
|
from app.routers.themes import router as themes_router
|
|
from app.services.download import DownloadService
|
|
|
|
logger = logging.getLogger("mediarip.app")
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI):
|
|
"""Application lifespan — initialise services on startup, tear down on shutdown."""
|
|
|
|
# --- Config ---
|
|
config_path = Path("config.yaml")
|
|
if config_path.is_file():
|
|
config = AppConfig(yaml_file=str(config_path))
|
|
logger.info("Config loaded from YAML: %s", config_path)
|
|
else:
|
|
config = AppConfig()
|
|
logger.info("Config loaded from defaults + env vars (no YAML file)")
|
|
|
|
# --- TLS warning ---
|
|
if config.admin.enabled:
|
|
logger.warning(
|
|
"Admin panel is enabled. Ensure HTTPS is configured via a reverse proxy "
|
|
"(Caddy, Traefik, nginx) to protect admin credentials in transit."
|
|
)
|
|
|
|
# --- Database ---
|
|
db = await init_db(config.server.db_path)
|
|
logger.info("Database initialised at %s", config.server.db_path)
|
|
|
|
# --- Event loop + SSE broker ---
|
|
loop = asyncio.get_event_loop()
|
|
broker = SSEBroker(loop)
|
|
|
|
# --- Download service ---
|
|
download_service = DownloadService(config, db, broker, loop)
|
|
|
|
# --- Purge scheduler ---
|
|
scheduler = None
|
|
if config.purge.enabled:
|
|
try:
|
|
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
|
from apscheduler.triggers.cron import CronTrigger
|
|
from app.services.purge import run_purge
|
|
|
|
scheduler = AsyncIOScheduler()
|
|
scheduler.add_job(
|
|
run_purge,
|
|
CronTrigger.from_crontab(config.purge.cron),
|
|
args=[db, config],
|
|
id="purge_job",
|
|
name="Scheduled purge",
|
|
)
|
|
scheduler.start()
|
|
logger.info("Purge scheduler started: cron=%s", config.purge.cron)
|
|
except ImportError:
|
|
logger.warning("APScheduler not installed — scheduled purge disabled")
|
|
except Exception as e:
|
|
logger.error("Failed to start purge scheduler: %s", e)
|
|
|
|
# --- Store on app.state ---
|
|
app.state.config = config
|
|
app.state.db = db
|
|
app.state.broker = broker
|
|
app.state.download_service = download_service
|
|
app.state.start_time = datetime.now(timezone.utc)
|
|
|
|
yield
|
|
|
|
# --- Teardown ---
|
|
if scheduler is not None:
|
|
scheduler.shutdown(wait=False)
|
|
download_service.shutdown()
|
|
await close_db(db)
|
|
logger.info("Application shutdown complete")
|
|
|
|
|
|
app = FastAPI(title="media.rip()", lifespan=lifespan)
|
|
app.add_middleware(SessionMiddleware)
|
|
app.include_router(admin_router, prefix="/api")
|
|
app.include_router(cookies_router, prefix="/api")
|
|
app.include_router(downloads_router, prefix="/api")
|
|
app.include_router(files_router, prefix="/api")
|
|
app.include_router(formats_router, prefix="/api")
|
|
app.include_router(health_router, prefix="/api")
|
|
app.include_router(sse_router, prefix="/api")
|
|
app.include_router(system_router, prefix="/api")
|
|
app.include_router(themes_router, prefix="/api")
|
|
|
|
# --- Static file serving (production: built frontend) ---
|
|
_static_dir = Path(__file__).resolve().parent.parent / "static"
|
|
if _static_dir.is_dir():
|
|
from fastapi.staticfiles import StaticFiles
|
|
from fastapi.responses import FileResponse
|
|
|
|
@app.get("/{full_path:path}")
|
|
async def serve_spa(full_path: str):
|
|
"""Serve the Vue SPA. Falls back to index.html for client-side routing."""
|
|
file_path = _static_dir / full_path
|
|
if file_path.is_file() and file_path.resolve().is_relative_to(_static_dir.resolve()):
|
|
return FileResponse(file_path)
|
|
return FileResponse(_static_dir / "index.html")
|
|
|
|
logger.info("Static file serving enabled from %s", _static_dir)
|