Добавлено описание проекта PVideoDl, включая функциональность, стек технологий, архитектуру, инструкции по запуску и API. Обновлён README.md для лучшего понимания проекта.

This commit is contained in:
Leonid Pershin
2026-06-19 12:07:55 +03:00
parent e15d0af056
commit 3281fa2a5d
43 changed files with 4699 additions and 0 deletions
View File
+25
View File
@@ -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
+53
View File
@@ -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))
+37
View File
@@ -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())