Проверено на Philips 24PFL4228T: потоки YouTube как есть не играют (ни видео, ни звук 44,1 кГц), перекодированные — играют; 70-минутный MP4 не открылся (индекс ~3 МБ), 70-минутный MKV открылся. Галочка в настройках видео, потолок качества 720p, битрейт ограничен лимитом размера файла. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
39 KiB
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_req30r/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 с, с SNIwww.google.comна том же адресе TLS поднимается, сwww.youtube.comмолчит. На сайте это выглядело как вечное «Проверяем…» при пустом журнале: yt-dlp ждёт по 30 с на запрос и повторяет, а запасной путь через egress вprobeвключался только по тексту бана (_BAN_SIGNATURES), таймаут туда не входит. Не режутwww.google.com,accounts.google.com,www.googleapis.com. По таблицеperfYouTube не работал около 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. Прочитай его и соблюдай в каждом сеансе; менять правила только там.