Проверено на Philips 24PFL4228T: потоки YouTube как есть не играют (ни видео, ни звук 44,1 кГц), перекодированные — играют; 70-минутный MP4 не открылся (индекс ~3 МБ), 70-минутный MKV открылся. Галочка в настройках видео, потолок качества 720p, битрейт ограничен лимитом размера файла. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
408 lines
39 KiB
Markdown
408 lines
39 KiB
Markdown
# AGENTS.md — карта развёртывания и инфраструктуры
|
||
|
||
Заметки для будущих агентов: что где стоит на боевом хосте. Код живёт в этом
|
||
репозитории, но прод и обход блокировок настроены снаружи — здесь их адреса.
|
||
|
||
> **Адреса здесь — заглушки.** Репозиторий публичный (Forgejo и зеркало на
|
||
> GitHub), а настоящие адреса хоста выдают, где стоит yt.zapret.moe. Значения
|
||
> `<ОСНОВНОЙ_IP_ХОСТА>`, `<IP_ДЛЯ_YOUTUBE>`, `<IP_AI_EGRESS>` — только в закрытом
|
||
> `~/pve-admin/servers.md` на хосте; реальные юниты — в `/etc/systemd/system/`.
|
||
> В этот репозиторий адреса не записывать (решение владельца 2026-10-17).
|
||
|
||
## Боевой сервис
|
||
|
||
- **Домен:** `yt.zapret.moe` → nginx → `http://127.0.0.1:8090`.
|
||
- **systemd-юнит:** `/etc/systemd/system/ytdlp-gui.service` (`ytdlp-gui.service`).
|
||
- `WorkingDirectory=/home/codex-pve/ytdlp-gui` — прод крутится **из этого
|
||
репозитория**. Любой `git`-коммит в файлы приложения попадёт в прод при
|
||
следующем `systemctl restart ytdlp-gui`.
|
||
- gunicorn: `--workers 1 --threads 64` (один процесс: **состояние задач
|
||
держится в памяти**, поэтому рестарт стирает очередь и текущие загрузки).
|
||
- Перезапуск: `sudo systemctl restart ytdlp-gui`.
|
||
- **Работает от системного пользователя `ytgrab`, не от `codex-pve`.** На
|
||
хосте стоит VPN-замок `ccw-vpn` (`ccw-vpn-lock.service`, `/etc/ccw-vpn`):
|
||
весь трафик uid 1000 (`codex-pve`) идёт в туннель `hytun`, а мимо него —
|
||
сброс. Под `codex-pve` привязка к <IP_ДЛЯ_YOUTUBE> давала «Connection reset by
|
||
peer» на любой запрос к YouTube (29.09.2026). Доступ `ytgrab` к репо —
|
||
ACL: `x` на `/home/codex-pve`, `rX` на репо, `rwX` на `downloads/` и
|
||
`data/` (с default-ACL для новых файлов). Проверять связь — от его имени:
|
||
`sudo -u ytgrab curl --interface <IP_ДЛЯ_YOUTUBE> https://ipinfo.io/ip`; curl от
|
||
`codex-pve` покажет выход VPN, а не прод.
|
||
- **nginx:** `/etc/nginx/sites-available/yt.zapret.moe`. Лимиты: `limit_conn 24`
|
||
на IP, `limit_req` 30r/m (heavy) и 240r/m (read).
|
||
|
||
### Ключевые параметры (в юните, через `Environment=`)
|
||
|
||
| Переменная | Значение | Смысл |
|
||
|---|---|---|
|
||
| `YTG_MAX_CONCURRENT` | **32** | одновременных загрузок (воркер-потоков) |
|
||
| `YTG_QUEUE_MAX` | 50 (дефолт) | сверх 32 ждут в очереди, потом 503 |
|
||
| `YTG_TASKS_MAX` | 500 (дефолт) | потолок записей задач |
|
||
| `YTG_MAX_FILESIZE_MB` | 2048 | максимум на файл |
|
||
| `YTG_CLIP_SOURCE_MAX_MB` | 4096 (дефолт) | только откат отрезка YouTube (HLS-дорожки): ролик качается целиком — потолок на целый; результат ≤ `MAX_FILESIZE_MB` |
|
||
| `YTG_DISK_QUOTA_MB` | **51200** | квота на папку загрузок (50 ГБ, с 13.10.2026; было 5 ГБ — в неё влезало два с половиной файла предельного размера); при превышении старые готовые файлы вытесняются (`_evict_for_quota`), а не дают отказ |
|
||
| `YTG_EVICT_MIN_AGE_SEC` | 300 (дефолт) | файл моложе вытесняется в последнюю очередь |
|
||
| `MemoryMax` / `TasksMax` | 4G / 2048 | лимиты cgroup (юнит); процессы загрузок считаются сюда же |
|
||
|
||
> Хост — **гипервизор Proxmox с ВМ**. Юнит намеренно ограничивает ресурсы
|
||
> (`CPUWeight=20`, `IOWeight=20`, `Nice=10`) и запрещает исходящие во внутреннюю
|
||
> сеть (`IPAddressDeny` на 10/8, 172.16/12, 192.168/16 и т.д.) — не снимать.
|
||
|
||
### Как это работает внутри
|
||
|
||
- **Каждая загрузка — отдельный процесс** `python -m jobrunner` (в `ps` их видно).
|
||
gunicorn держит только очередь и состояние задач (`downloader.DownloadManager`),
|
||
а исполнитель получает задачу JSON'ом в stdin и шлёт события JSON-строками в
|
||
stdout (`preparing` → `downloading` → `processing` → `done`). Так 32 загрузки не
|
||
делят один GIL и одну кучу (раньше пик памяти упирался в MemoryMax и шёл своп).
|
||
- **Сторож в менеджере:** нет прогресса `YTG_STALL_SEC` (180 с) — задача снимается
|
||
с понятной ошибкой; отмена — SIGTERM, через `YTG_CANCEL_GRACE_SEC` — SIGKILL всей
|
||
группы процессов (вместе с ffmpeg). Склейке/MP3 даётся `YTG_PROCESSING_TIMEOUT_SEC`.
|
||
- **Разбор ссылки (`probe`) по-прежнему в процессе gunicorn** — он короткий. Его
|
||
результат кэшируется в памяти (`downloader.cache_info`, 10 мин, ≤50 записей,
|
||
~130 КБ на ролик без автосубтитров) и отдаётся исполнителю загрузки — тот не
|
||
разбирает ролик заново (−1…1,5 с, и вдвое меньше запросов к YouTube). Кэш
|
||
берётся только при том же пути (напрямую/egress): ссылки привязаны к IP.
|
||
- Один файл на задачу: имя на диске `<task_id>.<ext>`, отдача через `/api/file`.
|
||
- Здоровье и метрики (только с localhost): `curl -s http://127.0.0.1:8090/readyz`
|
||
→ `queue_pending`, `tasks_active`, `downloads_mb`, `free_disk_mb`,
|
||
`youtube_direct` (`ok`, с какого времени, давность проверки).
|
||
`/healthz` — грубый «жив». Поле `warnings` — деградации, при которых сервис
|
||
всё же работает; `["youtube_direct"]` = YouTube напрямую не отвечает (ниже).
|
||
- **Сторож прямого пути к YouTube** (`downloader.yt_direct_ok`, 13.10.2026):
|
||
раз в минуту (из уборщика) короткий запрос `HEAD /generate_204` к
|
||
`www.youtube.com` с `YTG_YT_SOURCE_IP`, таймаут 4 с, отказ — две неудачи
|
||
подряд. Пока путь молчит, разбор ссылок и списков YouTube и сами загрузки
|
||
сразу идут через egress (`yt_egress_fallback`; путь выбирает сервер, а не
|
||
признак от клиента), перепроверка — раз в 20 с. В журнале по строке на
|
||
смену состояния: `journalctl -u ytdlp-gui | grep '\[youtube\]'`. `/readyz`
|
||
при этом остаётся `ok` (остальные сайты работают, YouTube — медленнее, через
|
||
пул) и несёт `warnings`. Выключатель: `YTG_YT_REACH_CHECK=0`. Оповещений
|
||
наружу нет: `/readyz` никто не опрашивает.
|
||
|
||
## Обход блокировок (DPI по SNI)
|
||
|
||
Провайдерский DPI (ТСПУ) рвёт TLS-хендшейк по имени хоста в SNI — из-за этого
|
||
`soundcloud.com`, `x.com` и др. не открывались (`_ssl.c:1012 handshake timed
|
||
out`). Диагностика: `openssl s_client -connect <ip>:443 -servername <host>` —
|
||
если конкретный SNI даёт `Terminated`, а другой на том же IP `CONNECTED`, это
|
||
SNI-DPI, не блокировка IP.
|
||
|
||
- **Инструмент:** zapret2 (`nfqws2`), сервис `zapret-egress.service`,
|
||
каталог `/opt/zapret-egress`. Документация: github.com/bol-van/zapret2.
|
||
- **Механизм:** nftables (`nft-apply.sh`, таблица `zapret_egress`) заворачивает
|
||
первые пакеты исходящих TCP/443-потоков в nfqueue 200; `nfqws2` применяет
|
||
десинхронизацию TLS ClientHello (`multidisorder`) **только для хостов из
|
||
списка**.
|
||
- **Список хостов:** `/opt/zapret-egress/egress-hosts.txt` (root). Матчатся хост
|
||
и все поддомены. Чтобы разблокировать новый сайт — **дописать его домен сюда**
|
||
и `sudo systemctl restart zapret-egress`.
|
||
- Уже добавлены: `soundcloud.com`, `sndcdn.com`, `twimg.com`,
|
||
`instagram.com`, `cdninstagram.com`, `fbcdn.net`, `pornhub.com`,
|
||
`phncdn.com` (плюс исходные `t.me`, `vkvideo.ru`, `xvideos.com` и т.д.).
|
||
- **У X свой список и своя стратегия** (01.10.2026):
|
||
`/opt/zapret-egress/egress-hosts-x.txt` (`x.com`, `twitter.com`, `t.co`) —
|
||
первый профиль в юните, `hostfakesplit:tcp_ts=-1000:repeats=1`; общий список
|
||
— второй профиль (`--new`), прежний `multidisorder`. **Не возвращать эти
|
||
домены в общий список.** `x.com`/`api.x.com` живут на двух адресах Cloudflare,
|
||
DNS отдаёт их по очереди: на `172.66.0.227` всё открывается и без обхода, а
|
||
на `162.159.140.229` провайдер пропускает ClientHello и глушит поток уже
|
||
ПОСЛЕ рукопожатия (0 байт ответа), `multidisorder` там не проходит даже
|
||
рукопожатие. Отсюда «Проверяем…» и ошибка через раз: `SSL:
|
||
UNEXPECTED_EOF_WHILE_READING`, `RemoteDisconnected`, `Read timed out` на
|
||
«Downloading guest token / GraphQL JSON». Стратегия опирается на TCP
|
||
timestamps (`net.ipv4.tcp_timestamps=1`): подложный сегмент сервер
|
||
отбрасывает по PAWS.
|
||
- **У YouTube тоже свой список и своя стратегия** (13.10.2026):
|
||
`/opt/zapret-egress/egress-hosts-yt.txt` (`youtube.com`, `youtu.be`,
|
||
`ytimg.com`, `ggpht.com`, `youtubei.googleapis.com`) — второй профиль в
|
||
юните, `hostfakesplit_multi:hosts=google.com,vimeo.com:tcp_ts=-1000:tcp_md5:repeats=2`
|
||
(стратегию выбрал владелец). До этого дня YouTube в списках не было вовсе:
|
||
провайдер его SNI не трогал, обходили только `googlevideo.com`. 13.10.2026
|
||
он начал молча глотать ClientHello с этими именами — TCP к `142.251.15x.4`
|
||
проходит за 0,02 с, с SNI `www.google.com` на том же адресе TLS поднимается,
|
||
с `www.youtube.com` молчит. На сайте это выглядело как вечное «Проверяем…»
|
||
при пустом журнале: yt-dlp ждёт по 30 с на запрос и повторяет, а запасной
|
||
путь через egress в `probe` включался только по тексту бана
|
||
(`_BAN_SIGNATURES`), таймаут туда не входит. Не режут `www.google.com`,
|
||
`accounts.google.com`, `www.googleapis.com`. По таблице `perf` YouTube
|
||
не работал около 16 часов (последняя удачная загрузка — в третьем часу
|
||
ночи, починено в 18:55), и ничто об этом не сообщило: `/readyz` оставался
|
||
`ok`. С того же дня за этим следит сторож прямого пути (см. «Как это
|
||
работает внутри»): строка `[youtube] напрямую не отвечает` в журнале =
|
||
пора подбирать стратегию, сайт тем временем работает через egress.
|
||
Быстрая проверка: `sudo -u ytgrab curl -m 8 --interface <IP_ДЛЯ_YOUTUBE>
|
||
https://www.youtube.com/ -o /dev/null -w '%{http_code}/%{size_download}\n'`
|
||
→ `200/95xxxx`; `000/0` при живом `https://www.google.com/` = снова SNI.
|
||
- `hostfakesplit_multi` — **не штатная** функция zapret2, а из
|
||
`/opt/zapret-egress/lua/zapret-multishake.lua` (подключён третьим
|
||
`--lua-init`; исходник — `zapretgui/private_zapretgui/dist/lua/`). При
|
||
обновлении zapret2 файл надо сохранить. `nfqws2` запускает Lua уже со
|
||
сброшенными правами (uid 1): файл должен читаться всем, вместе с
|
||
каталогами на пути.
|
||
- Запас, если multishake сломается: на стенде в тот же день YouTube
|
||
пробивали 31 стратегия из 62 — весь `multidisorder` (включая общую
|
||
`pos=1,midsld`, то есть хватило бы и просто дописать домены в общий
|
||
список), весь `hostfakesplit`, `fake` с `tcp_ts`/`tcp_md5`; простой
|
||
`multisplit` — нет.
|
||
- **Проверять полным HTTP-запросом, а не рукопожатием.** `openssl s_client …
|
||
CONNECTED` и удачный TLS-хендшейк здесь ничего не доказывают (см. выше): нужен
|
||
ответ с телом, на КАЖДОМ адресе хоста, несколько раз подряд —
|
||
`sudo -u ytgrab curl --resolve x.com:443:162.159.140.229 https://x.com/ -o
|
||
/dev/null -w '%{http_code}/%{size_download}\n'` → `200/35xxx`; затем
|
||
`yt-dlp --simulate <url>` раз десять (один успех — не показатель: мог
|
||
выпасть «хороший» адрес). Проверять от `ytgrab` или root: трафик
|
||
`codex-pve` уходит в VPN.
|
||
- **Стратегия перестала работать** — подобрать новую, не трогая прод:
|
||
`sudo python3 /opt/zapret-egress/strattest.py /opt/zapret-egress/strattest-strategies.txt
|
||
<ip> <sni> / <мин. байт ответа> <повторов>`. Стенд поднимает второй `nfqws2`
|
||
(очередь 201) и отдельную таблицу nft, которая ловит только сокеты с меткой
|
||
`0x00100000`; боевая очередь 200 не затрагивается, таблица убирается сама.
|
||
01.10.2026 из 55 стратегий адрес `162.159.140.229` пробивали 10:
|
||
`hostfakesplit` (с `tcp_md5`/`tcp_ts`), `fake` с `tcp_ts=-1000` или
|
||
`tcp_seq=1000000`, `multisplit:pos=10,midsld:seqovl=1`; весь `multidisorder`
|
||
и простой `multisplit` — нет.
|
||
- Проверка после добавления сайта в общий список: полный запрос (как выше), затем
|
||
`yt-dlp --simulate <url>`.
|
||
|
||
## YouTube: отдельный IP <IP_ДЛЯ_YOUTUBE> (главное)
|
||
|
||
Хостер (SERV.HOST, после обращения 22.09.2026) выдал дополнительный IP
|
||
**<IP_ДЛЯ_YOUTUBE>** «для доступа к YouTube». Весь прямой трафик к YouTube (разбор в
|
||
`probe`, загрузка в jobrunner, racefd и его повторные разборы) идёт с него
|
||
(`YTG_YT_SOURCE_IP` в юните сервиса; пусто — выкл). Остальные сайты — с
|
||
основного <ОСНОВНОЙ_IP_ХОСТА>.
|
||
|
||
- На основном IP YouTube держит антибот («not a bot»), на новом — нет.
|
||
- Новому IP YouTube выдаёт другие кластеры видеосерверов (напр. `sn-n8v7znz7`),
|
||
которые провайдер не режет; 8 соединений кусками — ~100 МБ/с. Серверы, что
|
||
провайдер режет по адресу назначения, заблокированы и с нового IP — обходы
|
||
ниже (racefd, hostban, egress-пул) остаются страховкой.
|
||
- Адрес на хосте: `ytgrab-youtube-ip.service` (`ip addr replace <IP_ДЛЯ_YOUTUBE>/32
|
||
dev vmbr0`, маршруты и мосты не трогает) + `ytgrab-youtube-ip-check.timer`
|
||
раз в 5 мин возвращает адрес, если его сняла перезагрузка сети Proxmox
|
||
(ifreload убирает адреса, которых нет в /etc/network/interfaces — сам
|
||
interfaces намеренно не правили: ошибка там оставит гипервизор без сети).
|
||
- Проверка: `curl --interface <IP_ДЛЯ_YOUTUBE> https://ipinfo.io/ip` → <IP_ДЛЯ_YOUTUBE>;
|
||
`yt-dlp --source-address <IP_ДЛЯ_YOUTUBE> --simulate <ролик>` → без «not a bot».
|
||
|
||
## YouTube: замедление googlevideo и свой загрузчик (racefd.py)
|
||
|
||
Сами ролики YouTube отдают серверы `rr*---sn-*.googlevideo.com`. Провайдерский
|
||
DPI к ним **пропускает пачку новых TLS-соединений после паузы, а следующие
|
||
вешает на рукопожатии** (TCP проходит, TLS молчит; с любым SNI). Установленное
|
||
соединение работает в полную скорость. Отсюда «висит на подготовке», а потом
|
||
~1 МБ/с: штатный загрузчик yt-dlp ждёт по 20 с на каждое новое соединение.
|
||
|
||
- **zapret не помогает:** перебор 10 стратегий nfqws2 (multidisorder, multisplit,
|
||
fake с md5/badsum/ts/autottl/seq, seqovl) дал тот же ~1 из 16, что и без
|
||
обхода. `googlevideo.com` в `egress-hosts.txt` оставлен по просьбе владельца
|
||
(не мешает: `manifest`/`redirector` открываются за 0,03 с).
|
||
- **egress не вариант:** SSH-SOCKS до <IP_AI_EGRESS> тянет ~220 КБ/с (напрямую — 27 МБ/с).
|
||
- **aria2c не вариант:** он сначала одним соединением узнаёт размер — те же минуты.
|
||
- **Решение — `racefd.RaceFD`:** подменяет загрузчик yt-dlp для https-файлов с
|
||
googlevideo известного размера (без прокси). Сразу пачкой открывает 12
|
||
соединений, бережёт их (keep-alive, переиспользуются между видео и звуком,
|
||
недочитанный ответ не бросается), качает кусками по 1 МиБ; хвост «добивают»
|
||
дублированием. Итог: 1080p + звук (105 МБ) за 6–9 с вместо минут простоя.
|
||
- **Режут по IP конкретного видеосервера, а не по SNI:** к одному `rr*` новые
|
||
соединения не проходят вовсе, к соседнему в тот же момент — все. Поэтому
|
||
RaceFD, когда живых соединений меньше половины, в фоне заново разбирает
|
||
ролик (`_fresh_url`) и добавляет «зеркало» — тот же формат и размер на другом
|
||
сервере; новые соединения идут к серверу с лучшим счётом. Проверено также
|
||
`hostfakesplit:host=ozon.ru:tcp_ts=-1000:tcp_md5:repeats=4` — как без обхода.
|
||
Выключатель: `YTG_YT_PARALLEL=0`.
|
||
- **Общий список заблокированных серверов (`hostban.py`, `data/hostban.sqlite3`):**
|
||
если к IP видеосервера подряд `YTG_BAN_FAILS` (8) соединений не прошло ни
|
||
одно, IP заносится в список на `YTG_BAN_TTL_SEC` (15 мин). Все загрузки —
|
||
любых роликов — такой IP обходят: сразу берут тот же формат с другого
|
||
сервера (ссылки на все форматы кэшируются из каждого разбора), а если другого
|
||
пока нет — ждут до 6 с, пока параллельный разбор (3 за раунд) его найдёт.
|
||
Удачное соединение снимает бан. Список виден в `perf_report.py`.
|
||
- **Запасной путь через egress-пул:** если все прямые серверы ролика в бане
|
||
(YouTube выдаёт на ролик всего 2-3 кластера, и бывает, что заблокированы все)
|
||
или 6 с нет ни байта, RaceFD разбирает ролик ЧЕРЕЗ egress (ссылка привязана к
|
||
IP запросившего) и качает куски через пул туннелей, по туннелю на поток;
|
||
прямые попытки идут параллельно. Провайдер режет каждое соединение до узла до
|
||
~300-400 КБ/с, поэтому туннелей 32: `ytgrab-egress-pool.target` →
|
||
`ytgrab-egress-pool@18110..18141.service` (SOCKS на 127.0.0.1, `ssh -D` к
|
||
<IP_AI_EGRESS>:10222, как основной туннель, от vpnbot не зависит). В юните сервиса
|
||
`YTG_EGRESS_POOL=socks5://127.0.0.1:18110-18141` — именно `socks5://`
|
||
(резолв у нас, IPv4): с `socks5h://` узел ходит к Google по своему IPv6, а
|
||
этот путь медленный (~0,2 МБ/с против 3-4 МБ/с по IPv4 на 12-16 потоках).
|
||
Процесс загрузки вообще работает только по IPv4 (`jobrunner._force_ipv4`). Проверка:
|
||
`curl -x socks5h://127.0.0.1:18115 https://ipinfo.io/ip` → <IP_AI_EGRESS>.
|
||
Зависимость: PySocks (в requirements.txt). Туннели пула проверяют связь каждые
|
||
10 с (`ServerAliveInterval=10`, `CountMax=3`): подвисший туннель ssh закрывает
|
||
примерно за 30 с, systemd поднимает заново. Поток загрузки после неудачи
|
||
переходит на следующий туннель пула сам, не дожидаясь этого.
|
||
**Бывает «живой труп»:** ssh держит связь (проверка проходит), а данные
|
||
через туннель не идут — такой systemd не перезапустит. 13.10.2026 им был
|
||
18110, первый в пуле, а разбор ролика через egress брал именно первый
|
||
(`EGRESS_POOL[0]`): загрузка висела на подготовке при 31 рабочем туннеле.
|
||
Теперь туннель для разбора выбирает `downloader.egress_pool_proxy` —
|
||
случайный, с проверкой, что соединение доходит до YouTube. Найти мёртвые:
|
||
`for p in $(seq 18110 18141); do curl -s -m 8 -x socks5://127.0.0.1:$p -o
|
||
/dev/null -w "$p %{http_code}\n" https://www.youtube.com/generate_204 & done;
|
||
wait` — не `204` лечится `sudo systemctl restart ytgrab-egress-pool@<порт>`
|
||
(по одному, не всем пулом — см. ниже про шторм).
|
||
- **Туннели не должны штормить.** sshd на ai-egress (OpenSSH 10) держит
|
||
`PerSourcePenalties`: соединения, не дошедшие до аутентификации, штрафуют
|
||
НАШ IP, и на время штрафа узел отказывает всем SSH-соединениям с хоста
|
||
(`journalctl -u ssh` на узле: «drop connection … penalty»). 27.09.2026 32
|
||
туннеля, перезапускавшиеся одновременно раз в 5 с, копили штраф сами:
|
||
296 перезапусков в час. Поэтому в юнитах: старт разнесён по экземплярам
|
||
(`ExecStartPre` — 0–15 с), пауза между перезапусками растёт 5 с → 180 с
|
||
(`RestartSteps`/`RestartMaxDelaySec`). Не проверять порт 10222 узла голым
|
||
TCP-подключением (`</dev/tcp/…`, `nc -z`) — это тоже штраф. Юниты пула и
|
||
туннеля — в `deploy/` (источник), ставятся копированием в
|
||
`/etc/systemd/system/` + `systemctl daemon-reload`; `tests/test_deploy_units.py`
|
||
держит эти свойства.
|
||
- **Антибот YouTube («Sign in to confirm you're not a bot»)** — YouTube может
|
||
перестать отдавать ролики нашему IP целиком (было 23.09.2026 после ночи
|
||
тестов с частыми повторными разборами). Распознаётся как бан (`_BAN_SIGNATURES`
|
||
+ «ролик без форматов» при `ignore_no_formats_error`): `probe` уходит через
|
||
egress, а загрузка идёт racefd целиком через пул (`_task_proxy` → первый
|
||
прокси пула, racefd в режиме egress_only). Чтобы не провоцировать антибот:
|
||
не больше 3 раундов повторного разбора на файл, по 2 разбора за раунд.
|
||
Проверка: `yt-dlp --simulate <ролик>` напрямую → «not a bot» = IP под антиботом.
|
||
- **Куски и соединения:** 12 соединений кусками по 1 МиБ; большие файлы (от
|
||
64 МБ, 4K) — кусками по 4 МиБ, 16 соединений напрямую / 32 через egress-пул.
|
||
YouTube ограничивает частоту запросов к одной ссылке и отвечает **HTTP 401**:
|
||
4K кусками 1 МиБ × 32 — 2 МБ/с и 1408 отказов из 1509, кусками 4 МиБ × 16 —
|
||
~100 МБ/с без отказов. На 401/429 поток ждёт 2 с. **HTTP 403** — не DPI, а
|
||
мёртвая ссылка: 6 подряд — ссылку бросаем (сервер в бан не идёт), новую
|
||
берём разбором; все мёртвые и раунды кончились — отказ сразу, исполнитель
|
||
разбирает ролик заново (раньше 5000 × 403 и 14 мин «обработки»). Виды ошибок соединений
|
||
пишутся в журнал: `journalctl -u ytdlp-gui | grep racefd`.
|
||
- **«Авто» = лучшее из лучших** (02.10.2026, `downloader.YT_FORMAT_SORT`,
|
||
общий с jobrunner): разрешение → fps → SDR выше HDR → https → язык/quality →
|
||
**битрейт видео (`vbr`)**. Дефолт yt-dlp ставит первым AV1, а YouTube отдаёт
|
||
AV1 самым урезанным (1080p 0,75 Мбит/с против 1,44 у H.264; 4K 10,5 против
|
||
16,2 у VP9). `br`/`tbr`/`size` в сортировку не ставить: у звука vbr нет, а с
|
||
ними выбирался дубляж или DRC вместо оригинала. Видео отбирается по лимиту
|
||
размера (`size_filter`: filesize/filesize_approx/tbr с запасом под звук) —
|
||
«Авто» берёт лучшее, что влезает в `MAX_FILESIZE_MB`; к отрезку не
|
||
применяется. Селекторы — `bv*+ba` (готовый файл со звуком тоже кандидат),
|
||
«1080p» у вертикальных — по ширине. probe отдаёт `picks` — что сервер
|
||
выберет на «Авто» и на каждую высоту; интерфейс показывает подпись и размер
|
||
по ним. Качество между роликами не запоминается — каждый начинается с «Авто».
|
||
- **Плейлисты и каналы YouTube** (`list=` без `v=`, `/@канал`, `/playlist`) —
|
||
`probe_list`: только список роликов (`extract_flat`, до 200), ~0,4 с; интерфейс
|
||
показывает список, клик открывает ролик. Раньше yt-dlp разбирал каждый ролик
|
||
списка — минуты «Проверяем…» и ошибка «нет форматов».
|
||
**Плейлист архивом** (`playlist: true` в /api/downloads): сервер сам заново
|
||
получает список (от клиента — только ссылка), не больше `YTG_PLAYLIST_DL_MAX`
|
||
(50) роликов, архив в пределах `MAX_FILESIZE_MB`. Исполнитель качает ролики
|
||
по очереди с паузой 1-3 с (разборы подряд провоцируют антибот), недоступные
|
||
пропускает (`ignoreerrors`), длиннее лимита — отсекает `match_filter`, и
|
||
собирает ZIP «001 - Название.ext» (`jobrunner.bundle_playlist`).
|
||
- **Посты с несколькими видео** (твит, карусель Instagram, пост boosty):
|
||
форматы у записей, а не у поста. `probe` отдаёт `multi_count`, сервер
|
||
(число — из своего разбора, `dl.multi_count`) качает их путём плейлиста:
|
||
все записи, ZIP «001 - Название.ext». Раньше — «Файл не найден после
|
||
скачивания». Смешанная карусель (фото + видео) — тоже пост, не галерея:
|
||
ролики путём плейлиста, фото следом (`POST_IMAGES_KEY`), номера сквозные
|
||
по карусели. Немые ролики (`has_audio: false`) — кнопка «Аудио» выключена.
|
||
- **Отрезок YouTube — «по запросу»** (`racefd.SectionPP` / `SectionSession`):
|
||
yt-dlp режет отрезок штатно (`download_ranges`, один ffmpeg с `-ss/-t` на
|
||
каждый вход, `-c copy` — без перекодирования), а ffmpeg по индексу файла
|
||
(sidx/Cues) читает только голову и сам отрезок. Напрямую к googlevideo
|
||
ffmpeg не пускаем (нет нашего IP, обходов DPI, зеркал): постпроцессор
|
||
`before_dl` подменяет ссылки дорожек на локальный HTTP-сервер
|
||
(`127.0.0.1`, случайный порт и токен), а тот отдаёт байты, которые RaceFD
|
||
в режиме «по запросу» качает в разреженный файл-подложку (в `/tmp`, удалён
|
||
сразу после открытия — мусора не остаётся). Повторные разборы и egress в
|
||
этом режиме — только если нужный кусок не идёт 2 с (простой ffmpeg — не
|
||
повод дёргать YouTube). Прогресс — сколько отдано ffmpeg. Замер: 4 мин
|
||
1080p из ролика в 3,5 ГБ — 25 МБ за 9 с. Режим «только через egress»
|
||
(антибот на нашем IP) тоже идёт по запросу через пул туннелей: 2 мин
|
||
1080p из 2,6 ГБ — ~10 с. Чтобы не тратить узкий egress: упреждение
|
||
ограничено оценкой конца отрезка, а кусок дублируется, только если по
|
||
нему молчат (1 с напрямую, 3 с через egress), а не просто медленно идёт. Если дорожки не для RaceFD (HLS),
|
||
отрезок снимается: ролик целиком + `jobrunner.trim_file`, потолок
|
||
`YTG_CLIP_SOURCE_MAX_MB`.
|
||
- **HLS-форматы** (616 и т.п., «премиальные») качает штатный загрузчик без смены
|
||
сервера, поэтому для YouTube `format_sort` предпочитает https при равном
|
||
разрешении (`jobrunner.YT_FORMAT_SORT`).
|
||
- **Метрики скорости:** каждая загрузка пишет строку в таблицу `perf` базы
|
||
статистики (`data/stats.sqlite3`): домен, исход, тайминги (очередь, до первого
|
||
байта, скачивание, обработка), скорость, сколько серверов понадобилось,
|
||
доля непробившихся соединений. Без ссылок и IP, хранится 30 дней
|
||
(`YTG_PERF_RETENTION_DAYS`). Сводка: `cd /home/codex-pve/ytdlp-gui &&
|
||
.venv/bin/python perf_report.py 24` (число — за сколько часов).
|
||
- Диагностика: `for i in $(seq 10); do curl -s -o /dev/null -w "%{time_appconnect}\n"
|
||
--max-time 5 https://rr5---sn-4g5lznle.googlevideo.com/; done` — пустые/нули =
|
||
соединение повисло.
|
||
|
||
## «Для старого телевизора» (18.10.2026)
|
||
|
||
Галочка в настройках видео: `tv: true` в `/api/downloads` → флаг `TV_KEY` в
|
||
`extra` → после загрузки исполнитель (`jobrunner.Job.transcode_tv`)
|
||
перекодирует файл в MKV: H.264 High@4.0 (B-кадры), не выше 720p, AAC-LC
|
||
48 кГц стерео. Только одиночное видео (не аудио, плейлист, эфир, фото).
|
||
|
||
- **Почему так.** Проверено на Philips 24PFL4228T (2013, USB, FAT32).
|
||
Потоки с YouTube «как есть» не играли: ни исходное видео (Main, B-кадры),
|
||
ни исходный звук AAC 44,1 кГц — играет только когда перекодированы ОБА
|
||
потока. 720p High с B-кадрами и AAC 48 кГц — играет.
|
||
- **MKV, а не MP4.** 70-минутный MP4 телевизор не открыл (индекс moov ~3 МБ),
|
||
10 и 25 минут играли; такой же 70-минутный MKV — играет. Не возвращать MP4
|
||
для длинных файлов, не резать на части.
|
||
- **Потолок качества 720p** (экран 1366×768): «Авто» и 1080+ при галочке
|
||
становятся 720p, меньшие высоты сохраняются. Битрейт — `tv_video_kbps`:
|
||
не выше 4 Мбит/с и так, чтобы результат влез в `MAX_FILESIZE_MB`.
|
||
- Нагрузка: перекодирование идёт в процессе исполнителя (под `CPUWeight=20`,
|
||
`Nice=10`), время ограничено `YTG_PROCESSING_TIMEOUT_SEC` (час без событий;
|
||
прогресс ffmpeg идёт событиями, так что сторож не срабатывает).
|
||
|
||
## Движок yt-dlp: обязательные спутники
|
||
|
||
- **curl_cffi** (в requirements.txt) — браузерный impersonate. Без него сайты
|
||
за Cloudflare/анти-ботом (kick.com, pornhub) отдают 403/404. yt-dlp
|
||
подхватывает автоматически.
|
||
- **deno** (в `/usr/local/bin`, есть в дефолтном PATH systemd) — JS-runtime для
|
||
nsig-челленджа YouTube. Без него в логах предупреждение «No supported
|
||
JavaScript runtime», часть форматов пропадает, скорость рвётся. Ставится
|
||
бинарём с github релизов; кэш пишет в `$HOME/.cache/deno` (HOME=/tmp,
|
||
PrivateTmp — писать можно).
|
||
- Диагностика «сайт не открывается»: `openssl s_client -connect <ip>:443
|
||
-servername <host>` → `Terminated` = SNI-DPI (добавить в egress-hosts.txt);
|
||
403/404 = нужен curl_cffi; 429/логин = ограничение сайта (напр. Instagram
|
||
stories требуют авторизации — не чинится).
|
||
|
||
## Ограничения
|
||
|
||
- **DRM не поддерживается** (Widevine/PlayReady/FairPlay): Netflix, Spotify,
|
||
Apple Music и пр. yt-dlp не расшифровывает — это не обходится настройкой.
|
||
- **Instagram stories/highlights** требуют авторизации (429 без кук); DPI
|
||
обойдён, но приватный контент без логина не берётся. Публичные посты и
|
||
фото-галереи работают (режим `kind=image` качает картинки в ZIP).
|
||
- **TikTok** блокирует серверные IP на уровне приложения («Your IP address is
|
||
blocked») — это НЕ DPI, zapret не помогает. Решено запасным egress-прокси
|
||
(см. ниже): probe при бане повторяет через него, TikTok работает.
|
||
|
||
## Запасной egress-прокси для забаненного контента
|
||
|
||
Некоторые сайты (TikTok и пр.) банят серверный IP на уровне приложения.
|
||
Механизм в коде: `probe` пробует напрямую и ТОЛЬКО при ошибке бана
|
||
(`_BAN_SIGNATURES`: IP-блок/гео/403) повторяет через `YTG_EGRESS_PROXY`;
|
||
если помогло, скачивание тоже идёт через него (флаг `via_egress`). Через
|
||
прокси гонится не весь трафик, а только забаненное.
|
||
|
||
- **Прокси:** SSH-туннель с динамическим SOCKS5 на `127.0.0.1:18099`, выход
|
||
с IP узла **ai-egress `<IP_AI_EGRESS>`** (чистый IP). Только петля — открытого
|
||
прокси наружу нет.
|
||
- **Сервис туннеля:** `ytgrab-egress-tunnel.service` (независим от vpnbot):
|
||
`ssh -p 10222 -i /root/.ssh/id_ed25519 -N -D 127.0.0.1:18099 root@<IP_AI_EGRESS>`,
|
||
`Restart=always`. Узел доступен по SSH на порту 10222 тем же root-ключом.
|
||
- **Подключение:** в `ytdlp-gui.service` задано
|
||
`Environment=YTG_EGRESS_PROXY=socks5h://127.0.0.1:18099` + мягкая
|
||
зависимость (`Wants=ytgrab-egress-tunnel.service`).
|
||
- Проверка: `curl -x socks5h://127.0.0.1:18099 https://ipinfo.io/ip` → должен
|
||
вернуть `<IP_AI_EGRESS>`.
|
||
- Рестарт прода теряет незавершённые задачи (состояние в памяти одного процесса).
|
||
|
||
## Оформление ответа
|
||
|
||
Правила оформления ответа общие для всех проектов: раздел «Оформление ответа» в `/home/codex-pve/AGENTS.md`. Прочитай его и соблюдай в каждом сеансе; менять правила только там.
|