2026-06-17 07:48:24 +00:00

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, devhttp://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) вводим только когда конкретная фича упрётся в текущую реализацию.

S
Description
No description provided
Readme MIT
214 KiB
Languages
Python 66.5%
Svelte 17.4%
TypeScript 11.3%
Shell 1.4%
CSS 1.3%
Other 2.1%