157 lines
8.3 KiB
Markdown
157 lines
8.3 KiB
Markdown
# 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` открывает <http://127.0.0.1:8000>, `dev` — <http://localhost:5173>.
|
||
Файлы складываются в `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
|
||
```
|
||
|
||
Открыть <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=` (пусто).
|
||
|
||
## Тесты
|
||
|
||
```bash
|
||
uv run pytest
|
||
```
|
||
|
||
## Задел на будущее
|
||
|
||
Эндпоинты `pause` / `resume` / `retry` и статус `PAUSED` лягут естественно —
|
||
модель и слои уже к ним готовы. Тяжёлую инфраструктуру (Redis, Celery, WebSocket,
|
||
Postgres) вводим только когда конкретная фича упрётся в текущую реализацию.
|