fix(downloaders): Deno runtime, свежий yt-dlp, честный health-check, смоук-тест
All checks were successful
CI/CD Pipeline / build-and-deploy (push) Successful in 2m36s

Возрастные YouTube-видео не скачивались: с cookies yt-dlp отбрасывает клиент
android и остаётся web, которому нужно решить n-challenge, а в образе стоял
Node 20 при требуемом минимуме 22 ("JS runtimes: node-20.19.2 (unsupported)").
Ставим Deno — рекомендованный yt-dlp рантайм и один статический бинарник.

Instagram падал с "empty media response" одинаково с cookies и без них —
дело было не в сессии, а в устаревшем экстракторе: слой pip был закеширован
на yt-dlp 2026.06.09. Поднимаем нижнюю границу до 2026.7.4.

/cookies/check помечал проблемой с cookies ЛЮБОЙ сбой, из-за чего на поломку
JS-рантайма прилетел алерт про протухшие cookies и увёл разбор не туда.
Теперь ответ содержит status: ok | cookies_invalid | extraction_failed |
no_cookies, и админ-бот шлёт разные сообщения. Разбор ответа в bot.py
сохраняет совместимость со старым форматом без поля status.

Добавлен smoke_test.py — гоняет реальные ссылки (включая обе регрессии выше)
через запущенные сервисы и печатает таблицу. Запускать после каждой правки.

Схема получения cookies переведена с крона на разовый ручной экспорт:
cookies-cron/ -> cookies/, удалены скрипты с анти-паттерном
`--cookies-from-browser BROWSER --cookies FILE`, который wiki yt-dlp прямо
запрещает и который сам ломал YouTube-сессию ротацией. Ключевые cookies живут
около года, поэтому обновление по расписанию не нужно — триггером служит алерт
health-check. deliver_cookies.sh только доставляет файлы по scp.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
vrubelroman 2026-07-26 00:31:16 +00:00
parent 5092d882e5
commit d597a5e1c5
19 changed files with 373 additions and 733 deletions

74
cookies/README.md Normal file
View file

@ -0,0 +1,74 @@
# cookies
Как обновлять `youtube_cookies.txt`/`instagram_cookies.txt` для прода и тестового стенда.
Куки обновляются **вручную и редко** — по алерту от проактивной проверки, а не по расписанию. Раньше здесь жил крон, который каждые 2030 минут экспортировал куки из постоянно запущенного браузера; эта схема убрана, ниже объяснено почему.
## Почему больше нет крона и постоянно открытого браузера
1. **Экспорт через `--cookies-from-browser` ломает YouTube-сессию.** Wiki yt-dlp прямо запрещает связку `--cookies COOKIEFILE --cookies-from-browser BROWSER` — именно она даёт ошибку `The provided YouTube account cookies are no longer valid. They have likely been rotated in the browser as a security measure`. Пока браузер держит живую сессию, YouTube её ротирует, и любой снятый снимок куков протухает.
2. **Обновлять раз в 30 минут нечего.** Ключевые куки живут около года: Instagram `sessionid` и YouTube `SID`/`__Secure-1PSID` на момент разбора имели срок ≈344376 дней. Крон сотни раз в сутки копировал одни и те же значения.
3. **Открытый браузер сам провоцировал блокировки.** Вкладка Instagram, висящая сутками, непрерывно ходит в сеть (лента, сторис, автоплей, телеметрия) — тысячи запросов, которые повышают шанс, что Instagram пометит сессию. Это ровно тот симптом, который мы ловили: `Main webpage is locked behind the login page` при формально валидных по сроку куках.
## Когда обновлять
Когда админ-бот пришлёт `🍪⚠️ ПРОАКТИВНАЯ ПРОВЕРКА COOKIES` по нужному сервису. Проверка живёт в `bot.py` (`check_cookies_health`, раз в 30 минут дёргает `POST /cookies/check` у youtube- и instagram-downloader) и шлёт алерт при переходе healthy→broken, потом не чаще раза в сутки, а при восстановлении — `✅ Cookies восстановлены`.
Проверить состояние вручную в любой момент:
```bash
ssh germany "curl -s -X POST localhost:5557/cookies/check" # YouTube
ssh germany "curl -s -X POST localhost:5556/cookies/check" # Instagram
```
## Экспорт cookies
Нужен браузер с графическим окружением и расширение для экспорта в формате `cookies.txt` (Netscape). На проде это сделать нельзя — там нет GUI.
### YouTube — обязательно через приватное окно
Официальный способ из wiki yt-dlp, именно он снимает ротацию:
1. Открыть **приватное окно** и залогиниться на YouTube.
2. В той же вкладке перейти на `https://www.youtube.com/robots.txt`. Это должна быть **единственная** открытая приватная вкладка.
3. Экспортировать куки `youtube.com` расширением в `youtube_cookies.txt`.
4. **Сразу закрыть приватное окно, не разлогиниваясь.** Сессия больше никогда не открывается — значит, YouTube её не ротирует, и куки живут месяцами.
Не использовать `--cookies-from-browser`: он заберёт обычные куки профиля, а не куки приватной сессии.
### Instagram
Экспортировать куки `instagram.com` расширением в `instagram_cookies.txt`, после чего вкладку закрыть и сессию в браузере не трогать. Формальной гарантии, как у YouTube, документация не даёт, но принцип тот же: сессию, которой браузер не пользуется, не ротируют и не помечают за подозрительную активность.
В файле должен быть `sessionid` — без него Instagram отдаёт `login required`:
```bash
grep -c sessionid instagram_cookies.txt # должно быть >= 1
```
### После экспорта
Браузер не оставлять запущенным. Профиль на диске для этой схемы больше не нужен — куки уже лежат в файлах.
## Доставка
Положить оба файла рядом с `deliver_cookies.sh` и запустить:
```bash
./deliver_cookies.sh
```
Скрипт проверит, что файлы непустые, и разольёт их по `scp`:
- на прод (`REMOTE_HOST=germany`) — обязательный шаг, при ошибке падает;
- на тестовый стенд (`DEV_HOST=hermesDev`) — необязательный, при недоступности пишет warning и продолжает.
Хосты и пути захардкожены под текущую инфраструктуру (SSH-алиасы из `~/.ssh/config` машины, где запускаете) — при переезде поправить переменные в начале скрипта.
Перезапускать контейнеры после доставки не нужно: `yt-dlp` читает файл с диска при каждом запросе. Монтирование в контейнеры — см. `docker-compose.prod.yml`/`docker-compose.yml`.
## Проверка результата
После доставки дёрнуть `/cookies/check` (команды выше) — ожидаем `"cookies_valid": true` у обоих сервисов. На следующем цикле проверки в админ-бот придёт `✅ Cookies восстановлены`.

94
cookies/deliver_cookies.sh Executable file
View file

@ -0,0 +1,94 @@
#!/bin/bash
# Доставка cookies на прод и тестовый стенд.
#
# Запускается ВРУЧНУЮ после разового экспорта cookies из браузера — не по крону.
# Как правильно экспортировать cookies, см. README.md рядом с этим скриптом.
# Автоматический экспорт через `yt-dlp --cookies-from-browser` убран намеренно:
# он ломает YouTube-сессию ("cookies have likely been rotated in the browser").
set -euo pipefail
# Определяем директорию скрипта
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
# Настройки
INSTAGRAM_COOKIES="$SCRIPT_DIR/instagram_cookies.txt"
YOUTUBE_COOKIES="$SCRIPT_DIR/youtube_cookies.txt"
REMOTE_HOST="germany"
REMOTE_INSTAGRAM_PATH="/root/services/videoDownloadTGbot/instagram_cookies.txt"
REMOTE_YOUTUBE_PATH="/root/services/videoDownloadTGbot/youtube_cookies.txt"
DEV_HOST="hermesDev"
DEV_INSTAGRAM_PATH="services/videoDownloadTGbot/instagram-downloader/instagram_cookies.txt"
DEV_YOUTUBE_PATH="services/videoDownloadTGbot/youtube-downloader/youtube_cookies.txt"
LOG_FILE="$SCRIPT_DIR/deliver.log"
# Функция логирования
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
}
# Функция обработки ошибок
error_exit() {
log "ОШИБКА: $*"
exit 1
}
# Проверяет, что файл существует и содержит хотя бы одну строку с cookie
check_cookies_file() {
local file="$1"
local name="$2"
if [ ! -f "$file" ]; then
error_exit "Файл не найден: $file. Сначала экспортируйте cookies $name — см. README.md"
fi
local lines
lines=$(grep -cv -e '^#' -e '^$' "$file" || true)
if [ "$lines" -eq 0 ]; then
error_exit "Файл $file пустой (0 строк с cookies). Экспорт $name не удался — см. README.md"
fi
log "$name: $file ($lines строк с cookies)"
}
log "=== Начало доставки cookies ==="
check_cookies_file "$INSTAGRAM_COOKIES" "Instagram"
check_cookies_file "$YOUTUBE_COOKIES" "YouTube"
# Копируем cookies на удаленный хост (прод)
log "Копирование cookies на удаленный хост $REMOTE_HOST..."
# Копируем Instagram cookies
if scp -q "$INSTAGRAM_COOKIES" "${REMOTE_HOST}:${REMOTE_INSTAGRAM_PATH}" 2>>"$LOG_FILE"; then
log "✅ Instagram cookies скопированы на $REMOTE_HOST"
else
error_exit "Ошибка при копировании Instagram cookies (код возврата: $?)"
fi
# Копируем YouTube cookies
if scp -q "$YOUTUBE_COOKIES" "${REMOTE_HOST}:${REMOTE_YOUTUBE_PATH}" 2>>"$LOG_FILE"; then
log "✅ YouTube cookies скопированы на $REMOTE_HOST"
else
error_exit "Ошибка при копировании YouTube cookies (код возврата: $?)"
fi
# Копируем cookies на тестовый стенд (не блокируем прод-доставку, если стенд недоступен)
log "Копирование cookies на тестовый стенд $DEV_HOST..."
if scp -q "$INSTAGRAM_COOKIES" "${DEV_HOST}:${DEV_INSTAGRAM_PATH}" 2>>"$LOG_FILE"; then
log "✅ Instagram cookies скопированы на $DEV_HOST"
else
log "⚠️ Не удалось скопировать Instagram cookies на $DEV_HOST (продолжаем)"
fi
if scp -q "$YOUTUBE_COOKIES" "${DEV_HOST}:${DEV_YOUTUBE_PATH}" 2>>"$LOG_FILE"; then
log "✅ YouTube cookies скопированы на $DEV_HOST"
else
log "⚠️ Не удалось скопировать YouTube cookies на $DEV_HOST (продолжаем)"
fi
log "=== Доставка cookies завершена успешно ==="
log "Проверить результат: ssh ${REMOTE_HOST} \"curl -s -X POST localhost:5557/cookies/check; curl -s -X POST localhost:5556/cookies/check\""
exit 0