# 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)`-имена наследуются: ```python # 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](app/services/extractors/google_drive.py). Если сайту нужен нестандартный процесс (HLS, сегменты) — наследуйся прямо от `Downloader` и переопредели `download()` целиком. ## Запуск — готовые скрипты В корне лежат скрипты-обёртки (Linux/macOS — `.sh`, Windows — `.bat`): ```bash # Прод: зависимости -> сборка фронта (если её нет) -> сервер ./run.sh # Windows: run.bat # Дев: бэкенд (:8000) + Vite dev-сервер (:5173) с hot-reload ./dev.sh # Windows: dev.bat ``` `run` открывает , `dev` — . Файлы складываются в `downloads/`. ## Запуск (прод, «для себя») — вручную Фронт собирается в статику, FastAPI отдаёт её с того же origin: ```bash # 1. зависимости uv sync cd frontend && npm install && npm run build && cd .. # 2. запуск uv run pvideodl ``` ## Запуск (дев) — два процесса ```bash # терминал 1 — бэкенд на :8000 uv run python -m app.main # терминал 2 — Vite dev-сервер на :5173 (проксирует /api на :8000) cd frontend && npm run dev ``` Открыть — с 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=` (пусто). ## Тесты ```bash uv run pytest ``` ## Задел на будущее Эндпоинты `pause` / `resume` / `retry` и статус `PAUSED` лягут естественно — модель и слои уже к ним готовы. Тяжёлую инфраструктуру (Redis, Celery, WebSocket, Postgres) вводим только когда конкретная фича упрётся в текущую реализацию.