120 lines
4.0 KiB
Python
120 lines
4.0 KiB
Python
"""Точка входа: сборка FastAPI-приложения, lifespan, роутеры, отдача статики."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from contextlib import asynccontextmanager
|
|
|
|
from fastapi import FastAPI
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from fastapi.responses import FileResponse
|
|
from fastapi.staticfiles import StaticFiles
|
|
from starlette.requests import Request
|
|
from starlette.responses import JSONResponse, Response
|
|
|
|
from app.api import downloads, events
|
|
from app.config import settings
|
|
from app.core.events import EventBus
|
|
from app.core.queue import DownloadQueue
|
|
from app.core.storage import SqliteStorage
|
|
from app.models import DownloadStatus
|
|
from app.services.worker import WorkerPool
|
|
|
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s")
|
|
logger = logging.getLogger("pvideodl")
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI):
|
|
storage = SqliteStorage(settings.db_path)
|
|
await storage.init()
|
|
|
|
queue = DownloadQueue()
|
|
event_bus = EventBus()
|
|
pool = WorkerPool(queue, storage, event_bus, settings.workers)
|
|
|
|
app.state.storage = storage
|
|
app.state.queue = queue
|
|
app.state.events = event_bus
|
|
app.state.pool = pool
|
|
|
|
# Перезаливаем в очередь всё, что осталось в pending с прошлого запуска.
|
|
for download in await storage.list():
|
|
if download.status == DownloadStatus.PENDING:
|
|
await queue.add(download.id)
|
|
|
|
await pool.start()
|
|
settings.download_dir.mkdir(parents=True, exist_ok=True)
|
|
logger.info("PVideoDl запущен. Файлы → %s", settings.download_dir)
|
|
try:
|
|
yield
|
|
finally:
|
|
await pool.stop()
|
|
await storage.close()
|
|
|
|
|
|
def create_app() -> FastAPI:
|
|
app = FastAPI(title="PVideoDl", version="0.1.0", lifespan=lifespan)
|
|
|
|
# В деве фронт крутится на Vite (5173) и ходит на /api — пускаем его.
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=["http://localhost:5173", "http://127.0.0.1:5173"],
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
app.include_router(downloads.router)
|
|
app.include_router(events.router)
|
|
|
|
@app.get("/api/health")
|
|
async def health() -> dict:
|
|
return {"status": "ok"}
|
|
|
|
_mount_frontend(app)
|
|
return app
|
|
|
|
|
|
def _mount_frontend(app: FastAPI) -> None:
|
|
"""Отдаём собранную статику SvelteKit, если она есть. SPA-fallback на index.html."""
|
|
static_dir = settings.static_dir
|
|
index = static_dir / "index.html"
|
|
if not index.exists():
|
|
@app.get("/")
|
|
async def no_frontend() -> JSONResponse:
|
|
return JSONResponse(
|
|
{
|
|
"message": "Фронтенд не собран. Соберите его (npm run build в frontend/) "
|
|
"или откройте API напрямую.",
|
|
"api": "/api/downloads",
|
|
}
|
|
)
|
|
return
|
|
|
|
app.mount("/_app", StaticFiles(directory=static_dir / "_app"), name="_app")
|
|
|
|
@app.get("/{full_path:path}")
|
|
async def spa(full_path: str, request: Request) -> Response:
|
|
# Несуществующий /api путь — это 404, а не SPA-страница.
|
|
if full_path.startswith("api/"):
|
|
return JSONResponse({"detail": "Not Found"}, status_code=404)
|
|
# Реальный файл из статики (favicon, изображения и т.п.) — отдаём как есть.
|
|
candidate = (static_dir / full_path).resolve()
|
|
if candidate.is_file() and static_dir.resolve() in candidate.parents:
|
|
return FileResponse(candidate)
|
|
return FileResponse(index)
|
|
|
|
|
|
app = create_app()
|
|
|
|
|
|
def run() -> None:
|
|
"""Запуск одной командой: `pvideodl` или `python -m app.main`."""
|
|
import uvicorn
|
|
|
|
uvicorn.run(app, host=settings.host, port=settings.port)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
run()
|