PVideoDl
Локальная скачивалка файлов и видео с веб-интерфейсом. Запускается одной командой, открывается в браузере, принимает список ссылок, качает в фоне и показывает прогресс в реальном времени.
- Прямые файлы (
.zip,.pdf,.mp4, …) качаются черезhttpxпо chunk'ам. - Видео с сайтов (YouTube и сотни других) — через
yt-dlp. - Прогресс течёт в UI по SSE в реальном времени.
- Три вкладки: Активные (что качается сейчас и завершилось за последний час), История (все завершённые загрузки) и Загрузчики (какие стратегии скачивания сейчас доступны).
- Если ссылка не прямой файл и ни один загрузчик её не поддерживает — загрузка завершается явной ошибкой (а не молчаливой попыткой угадать).
Стек
- Бэкенд: Python 3.12, FastAPI, Uvicorn, httpx, yt-dlp (+ curl_cffi для импперсонации), aiosqlite, sse-starlette
- Фронтенд: SvelteKit (SPA), TypeScript, Tailwind CSS v4, lucide-svelte
- Менеджер зависимостей:
uv
Архитектура
Реализации спрятаны за интерфейсами в app/core/ — роуты и сервисы зовут абстракции,
а не конкретику. Сегодня asyncio.Queue + SQLite, завтра Redis + Postgres — меняется
только начинка обёртки.
app/
main.py точка входа, lifespan, отдача статики
config.py настройки (env PVDL_*)
models.py Pydantic-модели
api/ роуты: downloads.py, events.py (SSE)
core/ абстракции: queue.py, storage.py, events.py (EventBus)
services/ downloader.py (реестр стратегий), worker.py (пул воркеров)
extractors/ расширения для сайтов с непрямыми ссылками
frontend/ SvelteKit SPA → собирается в frontend/build
Расширения: сайты с непрямыми ссылками
Выбор стратегии — цепочка обработчиков с приоритетами (pick_downloader):
кастомные экстракторы (priority > 0) перехватывают URL раньше встроенных
HttpxDownloader (прямые файлы) и YtDlpDownloader (универсальный фолбэк).
Добавить сайт = положить один файл в app/services/extractors/. Чаще всего хватает
резолвера — достать прямую ссылку (и при нужде Referer/Cookie), а
скачивание, прогресс и (n)-имена наследуются:
# app/services/extractors/my_site.py
import re
from app.services.downloader import Resolved, SiteExtractor, register
@register
class MySite(SiteExtractor):
priority = 100
@classmethod
def matches(cls, url: str) -> bool:
return "my-site.com/watch/" in url
async def resolve(self, url: str) -> Resolved:
# ... найти настоящую ссылку (запрос/скрейпинг) ...
return Resolved(download_url=real_url, headers={"Referer": url})
Модули пакета авто-загружаются на старте (load_extractors()). Готовый образец —
google_drive.py. Если сайту нужен
нестандартный процесс (HLS, сегменты) — наследуйся прямо от Downloader и
переопредели download() целиком.
Запуск — готовые скрипты
В корне лежат скрипты-обёртки (Linux/macOS — .sh, Windows — .bat):
# Прод: зависимости -> сборка фронта (если её нет) -> сервер
./run.sh # Windows: run.bat
# Дев: бэкенд (:8000) + Vite dev-сервер (:5173) с hot-reload
./dev.sh # Windows: dev.bat
run открывает http://127.0.0.1:8000, dev — http://localhost:5173.
Файлы складываются в downloads/.
Запуск (прод, «для себя») — вручную
Фронт собирается в статику, FastAPI отдаёт её с того же origin:
# 1. зависимости
uv sync
cd frontend && npm install && npm run build && cd ..
# 2. запуск
uv run pvideodl
Запуск (дев) — два процесса
# терминал 1 — бэкенд на :8000
uv run python -m app.main
# терминал 2 — Vite dev-сервер на :5173 (проксирует /api на :8000)
cd frontend && npm run dev
Открыть http://localhost:5173 — с hot-reload фронта.
API
| Метод | Путь | Назначение |
|---|---|---|
POST |
/api/downloads |
Добавить ссылки ({"urls": [...]}) |
GET |
/api/downloads |
Список всех загрузок |
DELETE |
/api/downloads/{id} |
Удалить задачу |
GET |
/api/downloaders |
Список загруженных стратегий |
GET |
/api/events |
SSE-поток обновлений прогресса |
GET |
/api/health |
Проверка живости |
Настройки (переменные окружения)
| Переменная | По умолчанию | Описание |
|---|---|---|
PVDL_DOWNLOAD_DIR |
./downloads |
Куда складывать файлы |
PVDL_DB_PATH |
./app.db |
Файл SQLite |
PVDL_STATIC_DIR |
./frontend/build |
Собранная статика фронтенда |
PVDL_WORKERS |
3 |
Сколько воркеров параллельно |
PVDL_HOST |
127.0.0.1 |
Хост сервера |
PVDL_PORT |
8000 |
Порт сервера |
PVDL_COOKIES_FROM_BROWSER |
— | Cookies для yt-dlp из браузера: chrome, firefox, edge… (можно chrome:Профиль) |
PVDL_COOKIES_FILE |
— | Cookies для yt-dlp из файла (формат Netscape cookies.txt) |
PVDL_IMPERSONATE |
chrome |
Импперсонация браузера для yt-dlp (chrome, edge, safari…); пусто — выключить |
Cookies нужны для сайтов, которые блокируют анонимные запросы (возрастной гейт, логин, гео). Достаточно одного из вариантов.
Сайты с анти-ботом по TLS-отпечатку (например PornHub, иначе
HTTP 410 Goneили обрыв соединения при скачивании видео) требуют импперсонации браузера. Она включена по умолчанию (PVDL_IMPERSONATE=chrome) черезcurl_cffiи применяется ко всем запросам yt-dlp. Выключить —PVDL_IMPERSONATE=(пусто).
Тесты
uv run pytest
Задел на будущее
Эндпоинты pause / resume / retry и статус PAUSED лягут естественно —
модель и слои уже к ним готовы. Тяжёлую инфраструктуру (Redis, Celery, WebSocket,
Postgres) вводим только когда конкретная фича упрётся в текущую реализацию.