Главное. Строка IPAddressAllow=any обесценивала весь список запретов ниже, и заявленная защита от SSRF существовала только на бумаге. Из cgroup сервиса оставались доступны Proxmox API на 10.20.0.1:8006 и соседняя виртуальная машина — проверено установлением соединения. Сбивало с толку то, что адрес метаданных 169.254.169.254 при этом блокировался, поэтому прежняя проверка выглядела успешной. Когда список состоит только из запретов, фильтр работает: внутренняя сеть закрыта, внешняя и DNS продолжают работать. Всё проверено отдельными запусками через systemd-run до применения на боевом юните. Петля не закрывается сознательно. Фильтр systemd не различает направление, и запрет 127.0.0.0/8 обрывает ВХОДЯЩИЕ соединения nginx к этому же сервису — проверено: слушатель внутри такой cgroup становится недоступен снаружи, то есть сайт лёг бы. Службы на 127.0.0.1 остаются достижимы; закрыть их можно, только переведя связку nginx→приложение на unix-сокет. Дисковый бюджет. Юнит вчетверо поднимал квоту относительно кода (20 ГБ против 5 ГБ) при 35 ГБ свободного на разделе гипервизора, где живут виртуальные машины. Приведено к 5 ГБ. /readyz больше не витрина. Наружу уходит только признак готовности; свободное место, глубина очереди и версия yt-dlp отдаются лишь локальному мониторингу — сайт публичный. Статика кэшируется на месяц: стили и восемь файлов шрифта отдаются этим же процессом, а потоки здесь дефицитны из-за SSE, и браузер ревалидировал все девять файлов на каждой навигации. Согласованы сроки хранения: карточка задачи считается от создания, а файл — от окончания скачивания, поэтому при равных TTL карточка истекала раньше файла, и файл оставался недостижимым, занимая квоту. Срок карточки поднят до 240 минут против 120 у файла. Также: .gitkeep больше не удаляется уборщиком по TTL (при подметании на старте он уже исключался), README приведён в соответствие с config.py — три значения по умолчанию были указаны неверно, тринадцать переменных не описаны вовсе. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AwY2Cg54RK7WZdvMaLV5Di
13 KiB
yt-grab
Веб-GUI для yt-dlp: вставил ссылку — выбрал качество, кодек и контейнер — скачал.
Сервер не хранит ничего. Настройки и история загрузок живут только в localStorage
браузера пользователя и никогда не уходят на сервер. Скачанный файл удаляется с диска
вскоре после того, как пользователь его забрал.
Возможности
- Превью ролика перед скачиванием: обложка, название, автор, длительность.
- Два режима выбора формата:
- простой — качество (до 2160p), контейнер MP4/WebM, аудио в MP3/AAC/Opus;
- полный — весь список форматов, которые yt-dlp отдал для конкретной ссылки.
- в простом режиме выбираются видеокодек (H.264 / VP9 / AV1 / любой), кодек звука (AAC / Opus / любой) и разрешение; для аудио — MP3 / AAC / Opus либо «Оригинал» без перекодирования.
- Прогресс в реальном времени через Server-Sent Events: проценты, скорость, ETA.
- Отмена загрузки, тёмная и светлая тема, адаптивная вёрстка.
- Страница
/statsс обезличенными счётчиками нагрузки: сколько запросов, скачиваний и байт отдано за сегодня / неделю / месяц / всё время.
Стек
| Слой | Технология | Почему |
|---|---|---|
| Бэкенд | Python 3 + Flask | yt-dlp — Python-библиотека, работаем с ней напрямую, а не через парсинг stdout |
| Прогресс | SSE | Поток односторонний (сервер → браузер); WebSocket избыточен |
| Фронтенд | Один HTML + CSS + vanilla JS | Без сборки и без node_modules; деплой — один процесс |
| Медиа | ffmpeg | Склейка video+audio и перекодирование аудио |
Установка
Нужны Python 3.11+ и ffmpeg в PATH.
git clone <repo> ytdlp-gui && cd ytdlp-gui
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
.venv/bin/python app.py # http://127.0.0.1:8090
Конфигурация
Всё через переменные окружения (значения ниже сверены с config.py):
Сеть и пути
| Переменная | По умолчанию | Назначение |
|---|---|---|
YTG_HOST / YTG_PORT |
127.0.0.1 / 8090 |
Адрес прослушивания |
YTG_DOWNLOAD_DIR |
./downloads |
Временная папка для файлов |
YTG_DATA_DIR |
./data |
Счётчики и лента (stats.sqlite3) |
YTG_TRUST_PROXY |
1 |
Брать реальный IP из X-Forwarded-For |
YTG_PROXY |
пусто | Прокси для исходящих запросов yt-dlp. Задаётся только администратором: принимать адрес прокси от пользователя нельзя, это SSRF |
Оба каталога перечислены в ReadWritePaths юнита. Если переопределяете их
окружением, поправьте и юнит — иначе при ProtectSystem=strict сервис упадёт
на первой же записи.
Нагрузка и пределы
| Переменная | По умолчанию | Назначение |
|---|---|---|
YTG_MAX_CONCURRENT |
3 |
Одновременных загрузок (размер пула воркеров) |
YTG_QUEUE_MAX |
50 |
Глубина очереди ожидания; сверх неё — честный 503 |
YTG_TASKS_MAX |
500 |
Потолок числа карточек задач в памяти |
YTG_CONCURRENT_FRAGMENTS |
5 |
Параллельных фрагментов DASH/HLS. Умножается на число загрузок |
YTG_MAX_FILESIZE_MB |
2048 |
Потолок размера одного файла |
YTG_MAX_DURATION_SEC |
14400 |
Потолок длительности ролика |
YTG_SSE_MAX_SECONDS |
900 |
Предел жизни одного SSE-соединения: оно занимает поток |
YTG_RATE_WINDOW_SEC |
60 |
Окно подсчёта обращений |
YTG_RATE_MAX_INFO |
20 |
Запросов метаданных на IP за окно |
YTG_RATE_MAX_DOWNLOAD |
6 |
Запусков загрузки на IP за окно |
Диск и сроки хранения
| Переменная | По умолчанию | Назначение |
|---|---|---|
YTG_DISK_QUOTA_MB |
5120 |
Квота на всю папку загрузок |
YTG_MIN_FREE_DISK_MB |
10240 |
Главный предохранитель: при меньшем свободном месте на разделе загрузки не принимаются и прерываются на ходу |
YTG_DELETE_AFTER_SERVE |
1 |
Удалять файл после отдачи |
YTG_SERVED_GRACE_SEC |
1800 |
Отсрочка удаления после выдачи (запас на докачку) |
YTG_FILE_TTL_MINUTES |
120 |
Жёсткий потолок жизни файла, от момента готовности |
YTG_TASK_TTL_MINUTES |
240 |
Срок карточки задачи. Держите больше FILE_TTL: карточка считается от создания задачи, а файл — от окончания скачивания |
YTG_JANITOR_INTERVAL_SEC |
30 |
Период работы уборщика |
Приватность и лента
| Переменная | По умолчанию | Назначение |
|---|---|---|
YTG_QUIET_ACCESS_LOG |
1 |
Не писать обращения в лог |
YTG_FEED_ENABLED |
1 |
Публичная лента «что скачивают». 0 полностью отключает запись ссылок |
YTG_FEED_PAGE_SIZE |
50 |
Размер страницы ленты |
Продакшен
sudo cp deploy/ytdlp-gui.service /etc/systemd/system/
sudo systemctl enable --now ytdlp-gui
sudo cp deploy/nginx-yt.zapret.moe.conf /etc/nginx/sites-available/yt.zapret.moe
sudo ln -s ../sites-available/yt.zapret.moe /etc/nginx/sites-enabled/
sudo certbot certonly --webroot -w /var/www/letsencrypt -d yt.zapret.moe
sudo nginx -t && sudo systemctl reload nginx
Запускается одним воркером gunicorn с несколькими потоками: состояние задач держится в памяти процесса, поэтому несколько воркеров его разорвут.
Важные детали nginx-конфига: для SSE-эндпоинта выключена буферизация
(proxy_buffering off), иначе прогресс копится в буфере и не доходит до браузера;
для отдачи файла — proxy_max_temp_file_size 0, чтобы многогигабайтные ответы
не складывались во временные файлы.
Безопасность
Сервис публичный, поэтому пользовательский ввод жёстко ограничен:
- Принимаются только схемы
http/https, а имя хоста резолвится и проверяется: приватные, loopback- и link-local адреса отклоняются. Без этого генерик-экстрактор yt-dlp ходил бы по внутренней сети и на169.254.169.254— то есть SSRF. - Опции yt-dlp не собираются из пользовательской строки. Простой режим строится из
фиксированного перечня, а в полном режиме
format_idпроходит через строгий regex^[A-Za-z0-9_\-.]+(\+[A-Za-z0-9_\-.]+)?$— он пропускает идентификаторы и связку137+140, но не синтаксис селекторов (скобки, фильтры,/, пробелы). - Выходная директория фиксирована; отдача файла — только по basename с проверкой
realpathпротив path traversal. - Лимиты: число одновременных загрузок, размер файла, длительность ролика, квота на диск, rate limit по IP (в приложении и в nginx).
- Доступ пользователя к
outtmpl,paths,exec,postprocessor_argsиcookiesfrombrowserне предоставляется ни в каком виде.
Приватность
-
URL и IP пользователей не пишутся в логи (
noprogress,--access-logfile /dev/null). -
История и настройки — только
localStorageв браузере. -
Файлы удаляются с сервера после выдачи; незабранные — по TTL. На диск сохраняются только два вида записей, оба в
data/stats.sqlite3: -
Обезличенные суточные счётчики — количество запросов, загрузок и сумма отданных байт. Ни IP, ни User-Agent.
-
Лента скачанного — адрес ролика, дата и счётчик. Это публикуется на
/stats. Ни названия, ни обложки, ни размера здесь нет: их показывает браузер посетителя, забирая напрямую с источника.
Связи между этими записями и конкретным человеком не хранится. Но адрес ролика
— это всё-таки данные: не вставляйте в сервис приватные ссылки с токенами,
они попадут в публичную ленту. Ленту можно выключить через YTG_FEED_ENABLED=0.
Заметки о поведении yt-dlp
Неочевидные вещи, на которые ушло время и которые закреплены в коде:
- Фильтры форматов регистрозависимы.
[vcodec^=avc1]не находитAVC1.640028, поэтому кодеки сравниваются регистронезависимым regex. При этом(?i)в начале выражения падает сPatternError— допустима только группа(?i:...). - VP9 приходит под двумя именами:
vp9(HLS) иvp09.xx(DASH), а^=vpзацепил бы ещё и VP8. Отсюда~='(?i:^vp0?9)'. - Значение фильтра нужно кавычить, если в нём есть что-то кроме
[\w.-]. [height<=1080]молча выбрасывает форматы с неизвестной высотой — нужен?после оператора:[height<=?1080].format_sortв долгоживущем процессе опасен:FormatSorter.settings— атрибут класса, и некорректный лимит портит сортировку на весь процесс gunicorn. Поэтому он здесь не используется.send_file+Response.call_on_closeне работает: включаетсяdirect_passthrough, и WSGI закрывает файловую обёртку, а неResponse, так что колбэк не вызывается. Удаление файла сделано через уборщик по отметке времени.restrictfilenamesвырезает всю кириллицу, из-за чего имя файла может выродиться в... Имя на диске задаётся из id задачи, а красивое уходит пользователю черезdownload_name.
Лицензия
MIT. Скачивайте только тот контент, на который у вас есть права.