yt-dlp-gui/README.md
codex-pve f1e4cdbe1d Запрет исходящих во внутреннюю сеть заработал: убран IPAddressAllow=any
Главное. Строка 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
2026-09-21 18:19:04 +03:00

13 KiB
Raw Permalink Blame History

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. Скачивайте только тот контент, на который у вас есть права.