Добавлено описание проекта 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
+66
View File
@@ -0,0 +1,66 @@
import tempfile
from pathlib import Path
import pytest
from httpx import ASGITransport, AsyncClient
from app.core.events import EventBus
from app.core.queue import DownloadQueue
from app.core.storage import SqliteStorage
from app.main import create_app
@pytest.fixture
async def client():
"""Приложение без воркеров и lifespan — состояние подкладываем вручную,
чтобы тесты роутов не качали реальные файлы."""
app = create_app()
storage = SqliteStorage(Path(tempfile.mkdtemp()) / "test.db")
await storage.init()
app.state.storage = storage
app.state.queue = DownloadQueue()
app.state.events = EventBus()
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as c:
yield c, app
await storage.close()
async def test_create_and_list(client):
c, app = client
resp = await c.post("/api/downloads", json={"urls": ["https://e.com/a.zip", "https://e.com/b.zip"]})
assert resp.status_code == 201
created = resp.json()
assert len(created) == 2
assert app.state.queue.qsize() == 2
resp = await c.get("/api/downloads")
assert resp.status_code == 200
assert len(resp.json()) == 2
async def test_create_empty_rejected(client):
c, _ = client
resp = await c.post("/api/downloads", json={"urls": [" ", ""]})
assert resp.status_code == 400
async def test_create_validation_requires_urls(client):
c, _ = client
resp = await c.post("/api/downloads", json={"urls": []})
assert resp.status_code == 422 # min_length=1
async def test_delete(client):
c, _ = client
created = (await c.post("/api/downloads", json={"urls": ["https://e.com/a.zip"]})).json()
did = created[0]["id"]
assert (await c.delete(f"/api/downloads/{did}")).status_code == 204
assert (await c.delete(f"/api/downloads/{did}")).status_code == 404
async def test_health(client):
c, _ = client
resp = await c.get("/api/health")
assert resp.status_code == 200 and resp.json()["status"] == "ok"
+72
View File
@@ -0,0 +1,72 @@
import tempfile
from pathlib import Path
import pytest
from app.core.storage import SqliteStorage
from app.models import Download, DownloadStatus
@pytest.fixture
async def storage():
path = Path(tempfile.mkdtemp()) / "test.db"
st = SqliteStorage(path)
await st.init()
yield st
await st.close()
async def test_create_get_list(storage: SqliteStorage):
d = Download(url="https://example.com/file.zip")
await storage.create(d)
got = await storage.get(d.id)
assert got is not None
assert got.url == d.url
assert got.status == DownloadStatus.PENDING
items = await storage.list()
assert [i.id for i in items] == [d.id]
async def test_update_returns_snapshot(storage: SqliteStorage):
d = Download(url="https://example.com/a.zip")
await storage.create(d)
snap = await storage.update(
d.id, status=DownloadStatus.DOWNLOADING, progress=42.5, downloaded_bytes=100
)
assert snap is not None
assert snap.status == DownloadStatus.DOWNLOADING
assert snap.progress == 42.5
assert snap.downloaded_bytes == 100
async def test_update_ignores_unknown_fields(storage: SqliteStorage):
d = Download(url="https://example.com/a.zip")
await storage.create(d)
snap = await storage.update(d.id, bogus="x", progress=10.0)
assert snap is not None and snap.progress == 10.0
async def test_delete(storage: SqliteStorage):
d = Download(url="https://example.com/a.zip")
await storage.create(d)
assert await storage.delete(d.id) is True
assert await storage.delete(d.id) is False
assert await storage.get(d.id) is None
async def test_downloading_marked_failed_on_init():
path = Path(tempfile.mkdtemp()) / "test.db"
st = SqliteStorage(path)
await st.init()
d = Download(url="https://example.com/a.zip", status=DownloadStatus.DOWNLOADING)
await st.create(d)
await st.close()
st2 = SqliteStorage(path)
await st2.init() # должен пометить "качающиеся" записи как FAILED
got = await st2.get(d.id)
assert got is not None and got.status == DownloadStatus.FAILED
await st2.close()