No description
  • Python 63.6%
  • HTML 25.9%
  • CSS 10.5%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
codex-pve 1885684e1a Исправлены четыре находки аудита: пагинация, сроки хранения, ссылки, плагин
Пагинация ленты. Листание шло через OFFSET, а last_seen меняется при
каждом скачивании: запись, обновлённая между запросами страниц, сдвигала
выборку, и одни записи попадали на две страницы, а другие не попадали ни
на одну. Теперь листание по ключу сортировки через непрозрачный курсор.
Одиночный индекс по count не покрывал вторую часть сортировки, поэтому
для «популярных» заведён составной.

Сроки хранения. Таблицы ленты и суточных счётчиков росли бессрочно, а
разовые миграции перечитывали всю ленту в память при КАЖДОМ старте —
время запуска росло вместе с таблицей. Добавлены сроки хранения и отметка
о выполненных миграциях: теперь они отрабатывают один раз.

Канонизация ссылок теряла порт, схему и якорь. Из-за этого ролики с
разных портов схлопывались в одну запись ленты, http-only сайт получал
нерабочую https-ссылку, а одностраничные приложения, где идентификатор
живёт в якоре, сливались все вместе. IPv6-литерал вдобавок терял скобки и
становился неразбираемым.

Плагин pmvhaven — два дефекта, оба с последствиями для безопасности:

  * Бралась ПЕРВАЯ подходящая ссылка со страницы без проверки адреса.
    Рекламной вставки или пользовательского текста хватало, чтобы увести
    загрузку на чужой адрес, в том числе внутренний: проверка адреса в
    приложении покрывает только ссылку ОТ пользователя, а не то, что
    выковыряно со страницы. Теперь кандидаты ранжируются, хост проверяется
    на публичность, плейлисты предпочитаются готовым файлам.

  * Шаблон исключал обратный слэш, поэтому экранированную форму
    (`https:\/\/...`) не находил вовсе, а строка с обратной заменой была
    недостижимым кодом. Вдобавок нежадный квантификатор обрывал ссылку на
    первом расширении, а в путях CDN оно встречается в середине
    (.../x.mp4/master.m3u8). Расширение проверяется после разэкранирования.

Добавлена страница исходников на GitHub как зеркало, знак взят из набора
Simple Icons под CC0 (сам знак остаётся товарным знаком GitHub и стоит
здесь только как ссылка на репозиторий). Заголовок секции с переключателем
рядом больше не распирает узкий экран.

Тестов 224: пагинация проверяется на том самом сценарии со сдвигом записи
между страницами, плагин — на экранированных ссылках и приватных адресах.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AwY2Cg54RK7WZdvMaLV5Di
2026-09-21 19:06:21 +03:00
deploy Запрет исходящих во внутреннюю сеть заработал: убран IPAddressAllow=any 2026-09-21 18:19:04 +03:00
downloads Веб-GUI для yt-dlp: бэкенд, интерфейс и деплой на yt.zapret.moe 2026-09-21 16:27:22 +03:00
static Исправлены четыре находки аудита: пагинация, сроки хранения, ссылки, плагин 2026-09-21 19:06:21 +03:00
templates Исправлены четыре находки аудита: пагинация, сроки хранения, ссылки, плагин 2026-09-21 19:06:21 +03:00
tests Исправлены четыре находки аудита: пагинация, сроки хранения, ссылки, плагин 2026-09-21 19:06:21 +03:00
yt_dlp_plugins Исправлены четыре находки аудита: пагинация, сроки хранения, ссылки, плагин 2026-09-21 19:06:21 +03:00
.gitignore Канонизация ссылок в ленте, эндпоинт /readyz и набор тестов 2026-09-21 18:03:51 +03:00
app.py Исправлены четыре находки аудита: пагинация, сроки хранения, ссылки, плагин 2026-09-21 19:06:21 +03:00
config.py Исправлены четыре находки аудита: пагинация, сроки хранения, ссылки, плагин 2026-09-21 19:06:21 +03:00
downloader.py Исправлены четыре находки аудита: пагинация, сроки хранения, ссылки, плагин 2026-09-21 19:06:21 +03:00
README.md Запрет исходящих во внутреннюю сеть заработал: убран IPAddressAllow=any 2026-09-21 18:19:04 +03:00
requirements-dev.txt Канонизация ссылок в ленте, эндпоинт /readyz и набор тестов 2026-09-21 18:03:51 +03:00
requirements.txt Веб-GUI для yt-dlp: бэкенд, интерфейс и деплой на yt.zapret.moe 2026-09-21 16:27:22 +03:00
stats.py Исправлены четыре находки аудита: пагинация, сроки хранения, ссылки, плагин 2026-09-21 19:06:21 +03:00

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