Добавлено описание проекта PVideoDl, включая функциональность, стек технологий, архитектуру, инструкции по запуску и API. Обновлён README.md для лучшего понимания проекта.
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
"""Доступ к разделяемым компонентам приложения через app.state.
|
||||
|
||||
Компоненты (Storage, DownloadQueue, EventBus, WorkerPool) создаются в lifespan
|
||||
в main.py и кладутся в app.state. Роуты достают их отсюда через Depends.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import Request
|
||||
|
||||
from app.core.events import EventBus
|
||||
from app.core.queue import DownloadQueue
|
||||
from app.core.storage import Storage
|
||||
|
||||
|
||||
def get_storage(request: Request) -> Storage:
|
||||
return request.app.state.storage
|
||||
|
||||
|
||||
def get_queue(request: Request) -> DownloadQueue:
|
||||
return request.app.state.queue
|
||||
|
||||
|
||||
def get_events(request: Request) -> EventBus:
|
||||
return request.app.state.events
|
||||
@@ -0,0 +1,53 @@
|
||||
"""Роуты управления загрузками: создание, список, удаление."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
|
||||
from app.api.deps import get_events, get_queue, get_storage
|
||||
from app.core.events import EventBus
|
||||
from app.core.queue import DownloadQueue
|
||||
from app.core.storage import Storage
|
||||
from app.models import CreateDownloads, Download, DownloadEvent
|
||||
|
||||
router = APIRouter(prefix="/api/downloads", tags=["downloads"])
|
||||
|
||||
|
||||
@router.post("", status_code=status.HTTP_201_CREATED, response_model=list[Download])
|
||||
async def create_downloads(
|
||||
payload: CreateDownloads,
|
||||
storage: Storage = Depends(get_storage),
|
||||
queue: DownloadQueue = Depends(get_queue),
|
||||
events: EventBus = Depends(get_events),
|
||||
) -> list[Download]:
|
||||
"""Принять ссылки (по одной на строку), создать задачи и поставить в очередь."""
|
||||
urls = [u.strip() for u in payload.urls if u.strip()]
|
||||
if not urls:
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST, "Список ссылок пуст")
|
||||
|
||||
created: list[Download] = []
|
||||
for url in urls:
|
||||
download = Download(url=url)
|
||||
await storage.create(download)
|
||||
await queue.add(download.id)
|
||||
await events.publish(DownloadEvent(type="created", download=download))
|
||||
created.append(download)
|
||||
return created
|
||||
|
||||
|
||||
@router.get("", response_model=list[Download])
|
||||
async def list_downloads(storage: Storage = Depends(get_storage)) -> list[Download]:
|
||||
"""Список всех загрузок — для гидрации UI при загрузке страницы."""
|
||||
return await storage.list()
|
||||
|
||||
|
||||
@router.delete("/{download_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_download(
|
||||
download_id: str,
|
||||
storage: Storage = Depends(get_storage),
|
||||
events: EventBus = Depends(get_events),
|
||||
) -> None:
|
||||
deleted = await storage.delete(download_id)
|
||||
if not deleted:
|
||||
raise HTTPException(status.HTTP_404_NOT_FOUND, "Загрузка не найдена")
|
||||
await events.publish(DownloadEvent(type="deleted", id=download_id))
|
||||
@@ -0,0 +1,37 @@
|
||||
"""SSE-поток обновлений прогресса: GET /api/events."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
from sse_starlette.sse import EventSourceResponse
|
||||
|
||||
from app.api.deps import get_events
|
||||
from app.core.events import EventBus
|
||||
|
||||
router = APIRouter(tags=["events"])
|
||||
|
||||
# Если за это время не было ни одного события — шлём комментарий-пинг,
|
||||
# чтобы прокси/браузер не закрыли «висящее» соединение.
|
||||
_KEEPALIVE_SECONDS = 15.0
|
||||
|
||||
|
||||
@router.get("/api/events")
|
||||
async def events_stream(
|
||||
request: Request,
|
||||
events: EventBus = Depends(get_events),
|
||||
) -> EventSourceResponse:
|
||||
async def generator():
|
||||
async with events.subscribe() as queue:
|
||||
while True:
|
||||
if await request.is_disconnected():
|
||||
break
|
||||
try:
|
||||
event = await asyncio.wait_for(queue.get(), timeout=_KEEPALIVE_SECONDS)
|
||||
except asyncio.TimeoutError:
|
||||
yield {"event": "ping", "data": "{}"}
|
||||
continue
|
||||
yield {"event": event.type, "data": event.model_dump_json()}
|
||||
|
||||
return EventSourceResponse(generator())
|
||||
Reference in New Issue
Block a user