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

View file

@ -182,7 +182,7 @@ CREATE TABLE stats (
- (YouTube) `./youtube_cookies.txt:/app/youtube_cookies.txt`
- (Instagram) `./instagram_cookies.txt:/app/instagram_cookies.txt`
Cookies монтируются с хоста (не read-only — внешний крон на отдельной машине обновляет файл по `scp`), а не запекаются в образ — это позволяет обновлять их без пересборки/передеплоя сервиса.
Cookies монтируются с хоста (не read-only — файл обновляется вручную по `scp` с машины, где есть браузер), а не запекаются в образ — это позволяет обновлять их без пересборки/передеплоя сервиса. Процедура — в `cookies/README.md`.
## Потоки данных
@ -235,7 +235,7 @@ YAPFILES_DOWNLOADER_URL=http://<host2_ip>:5558
## Безопасность
- Токен бота в `.env` (не коммитится)
- Cookies YouTube/Instagram закоммичены в репозиторий как fallback внутри образа; актуальные версии приходят на хост кроном и монтируются поверх (см. `README.md#cookies-на-проде`)
- Cookies YouTube/Instagram закоммичены в репозиторий как fallback внутри образа; актуальные версии кладутся на хост вручную и монтируются поверх (см. `README.md#cookies-на-проде` и `cookies/README.md`)
- Сервисы доступны только по указанным URL
- Можно добавить API key между сервисами

View file

@ -76,14 +76,9 @@ TIKTOK_DOWNLOADER_URL=http://localhost:5559
Если планируете скачивать видео с Instagram:
1. Экспортируйте cookies из браузера (см. `instagram-downloader/INSTAGRAM_COOKIES_INSTRUCTIONS.md`)
1. Экспортируйте cookies из браузера расширением для `cookies.txt` — процедура в [`cookies/README.md`](cookies/README.md)
2. Сохраните файл как `instagram_cookies.txt` в папке `instagram-downloader/`
```bash
cd instagram-downloader
./get_instagram_cookies.sh
```
### 4. Запуск сервисов
Все 7 сервисов (бот, admin-бот, 5 загрузчиков) собираются и запускаются одной командой из корня проекта — единый `docker-compose.yml` уже содержит build-контексты для всех подпапок:
@ -174,7 +169,21 @@ Content-Type: application/json
В проде `youtube_cookies.txt`/`instagram_cookies.txt` монтируются в контейнеры с хоста (`docker-compose.prod.yml`), а не запекаются в образ — это позволяет обновлять их без пересборки/передеплоя. `yt-dlp`/`app.py` читают файл с диска при каждом запросе — рестарт контейнера не требуется.
Куки получает по крону (раз в 20 минут) отдельная машина с графическим окружением и реальным залогиненным браузером (на самом проде это сделать нельзя — там нет GUI/браузера), и разливает результат по `scp` сразу на прод и на тестовый стенд. Скрипты для этого, требования к cron-машине и инструкция по развёртыванию на новой машине — в [`cookies-cron/`](cookies-cron/README.md). Это копия того, что реально крутится на cron-машине; `youtube-downloader/get_youtube_cookies.sh` и `instagram-downloader/get_instagram_cookies.sh` в подпапках сервисов — более старые версии тех же скриптов, с тех пор разошедшиеся.
Куки обновляются **вручную и редко** — по алерту `🍪⚠️ COOKIES ПРОТУХЛИ` из админ-бота, а не по расписанию. Ключевые куки живут около года, поэтому регулярный экспорт не нужен; за состоянием следит фоновая проверка в `bot.py` (`check_cookies_health`, раз в 30 минут дёргает `POST /cookies/check` у youtube- и instagram-downloader).
Экспорт делается с машины с браузером (на проде нет GUI) и требует точной процедуры — для YouTube только через приватное окно, иначе сессия ротируется и куки протухают. Пошаговая инструкция и скрипт доставки — в [`cookies/`](cookies/README.md).
## Смоук-тест
После любой правки, до коммита, на поднятом тестовом контуре:
```bash
python3 smoke_test.py
```
Гоняет реальные ссылки через запущенные сервисы и печатает таблицу: `/cookies/check` обоих загрузчиков, `/formats` на возрастном YouTube-видео (ловит отсутствие рабочего JS-рантайма) и реальные скачивания YouTube и Instagram. Код возврата `0` — всё прошло, `1` — есть падения. Адреса переопределяются через `YOUTUBE_DOWNLOADER_URL`/`INSTAGRAM_DOWNLOADER_URL`.
Если сервисы пересобирались — поднимать их через `docker compose up -d --force-recreate <service>`: без флага `up -d` просто стартует старый контейнер, и правка в проверку не попадёт.
## Обновление

43
bot.py
View file

@ -617,8 +617,13 @@ def _save_cookie_state(state: dict):
logger.error(f"Не удалось сохранить состояние cookie-health: {e}")
async def notify_admin_cookie_alert(service: str, healthy: bool, detail: str):
"""Проактивное уведомление о состоянии cookies (health-check), не связано с ошибкой конкретного пользователя"""
async def notify_admin_cookie_alert(service: str, status: str, detail: str):
"""Проактивное уведомление от health-check, не связано с ошибкой конкретного пользователя.
status: 'ok' | 'cookies_invalid' | 'extraction_failed'
Тексты разные намеренно: сбой извлечения (версия yt-dlp, JS-рантайм) не должен
выглядеть как протухшие cookies, иначе разбор уходит не туда.
"""
if not ADMIN_BOT_TOKEN:
return
admin_chat_id = get_admin_chat_id()
@ -634,15 +639,24 @@ async def notify_admin_cookie_alert(service: str, healthy: bool, detail: str):
)
admin_bot = Bot(token=ADMIN_BOT_TOKEN, request=request)
if healthy:
text = f"✅ Cookies восстановлены\n\n🍪 Сервис: {service}\nCookies снова рабочие."
if status == 'ok':
text = f"✅ Снова работает\n\n🍪 Сервис: {service}\nПроверка проходит успешно."
elif status == 'cookies_invalid':
text = (
f"🍪⚠️ COOKIES ПРОТУХЛИ\n\n🍪 Сервис: {service}\n{detail}\n\n"
"Обновить по инструкции в cookies/README.md "
"(для YouTube — только через приватное окно)."
)
else:
text = f"🍪⚠️ ПРОАКТИВНАЯ ПРОВЕРКА COOKIES\n\n🍪 Сервис: {service}\n{detail}"
text = (
f"🛠⚠️ СЕРВИС НЕ МОЖЕТ ИЗВЛЕЧЬ ВИДЕО\n\n🍪 Сервис: {service}\n{detail}\n\n"
"Это НЕ cookies — проверить версию yt-dlp и логи сервиса."
)
if len(text) > 4000:
text = text[:4000] + ""
await admin_bot.send_message(chat_id=admin_chat_id, text=text)
logger.info(f"Уведомление о состоянии cookies ({service}) отправлено админ боту")
logger.info(f"Уведомление health-check ({service}, {status}) отправлено админ боту")
except Exception as e:
logger.error(f"Ошибка при отправке уведомления о cookies админ боту: {e}")
@ -664,24 +678,31 @@ async def check_cookies_health():
logger.warning(f"Проверка cookies {service} не удалась (сервис недоступен?): {e}")
continue # не меняем состояние при сетевой ошибке — только при чистом ответе cookies_valid:false
if data.get('cookies_valid') is None:
# Обратная совместимость: старый ответ без 'status' различал только cookies_valid
status = data.get('status')
if status is None:
valid = data.get('cookies_valid')
status = 'no_cookies' if valid is None else ('ok' if valid else 'cookies_invalid')
if status == 'no_cookies':
continue # куки для этого сервиса не настроены — нечего проверять
currently_healthy = bool(data.get('cookies_valid'))
detail = data.get('detail', '')
currently_healthy = status == 'ok'
was_healthy = state.get(service, 'healthy') == 'healthy'
if not currently_healthy and was_healthy:
state[service] = 'broken'
state.setdefault('last_reminder_ts', {})[service] = time.time()
await notify_admin_cookie_alert(service, healthy=False, detail=data.get('detail', ''))
await notify_admin_cookie_alert(service, status, detail)
elif not currently_healthy and not was_healthy:
last = state.get('last_reminder_ts', {}).get(service, 0)
if time.time() - last >= COOKIE_REMINDER_INTERVAL:
state.setdefault('last_reminder_ts', {})[service] = time.time()
await notify_admin_cookie_alert(service, healthy=False, detail=data.get('detail', ''))
await notify_admin_cookie_alert(service, status, detail)
elif currently_healthy and not was_healthy:
state[service] = 'healthy'
await notify_admin_cookie_alert(service, healthy=True, detail='')
await notify_admin_cookie_alert(service, 'ok', '')
_save_cookie_state(state)

View file

@ -1,36 +0,0 @@
# cookies-cron
Скрипты для регулярного обновления `youtube_cookies.txt`/`instagram_cookies.txt` из реального авторизованного браузера и доставки их по `scp` на прод и тестовые стенды.
Это **точная копия** скриптов, которые сейчас реально работают по крону на отдельной машине (не на проде — на проде GUI/браузера нет в принципе). Здесь они лежат для справки и на случай, если cron-машину придётся поднимать заново. Ничего в этом репозитории/CI их не запускает и не использует автоматически.
## Где это должно стоять и работать
- Машина с **графическим окружением и реальным браузером** (используется Firefox, см. `BROWSER="firefox"` в обоих скриптах) — обычный десктоп/ноутбук, не сервер.
- В этом браузере должны быть **активные авторизованные сессии** на youtube.com и на instagram.com — скрипты не логинятся сами, они просто экспортируют куки из уже залогиненного браузера через `yt-dlp --cookies-from-browser`. Если сессии нет (вышел из аккаунта/куки протухли) — экспортируется пустой/неполный набор куков, и видео с этими доменами перестают скачиваться (в Instagram это проявляется как `login required` без всякой ошибки в самом cron-скрипте).
- На машине должен быть установлен **`yt-dlp`** и доступен в `PATH` для cron-окружения (не только для интерактивного шелла — у cron urezанный `PATH`). Скрипты исторически не проверяют, что `yt-dlp` реально нашёлся и отработал — при отсутствии `yt-dlp` они тихо «успешно завершаются», просто переиспользуя старый файл cookies. Если куки на проде не обновляются неделями без видимых ошибок — первым делом проверьте `which yt-dlp` на cron-машине.
## Файлы
- **`get_instagram_cookies.sh`** / **`get_youtube_cookies.sh`** — извлекают куки из Firefox в локальные `instagram_cookies.txt`/`youtube_cookies.txt` рядом со скриптом.
- **`update_cookies.sh`** — оркестратор: гоняет оба скрипта выше, затем разливает результат по `scp` на:
- прод (`REMOTE_HOST`/`REMOTE_INSTAGRAM_PATH`/`REMOTE_YOUTUBE_PATH`) — обязательный шаг, при ошибке скрипт падает с `error_exit`;
- тестовый стенд (`DEV_HOST`/`DEV_INSTAGRAM_PATH`/`DEV_YOUTUBE_PATH`) — необязательный, при недоступности стенда просто пишет warning и продолжает.
Значения этих переменных сейчас захардкожены под текущую инфраструктуру (SSH-алиасы `germany`/`hermesDev` из `~/.ssh/config` той машины, где крон запущен) — при разворачивании на новой cron-машине их нужно поправить под актуальные хосты/пути.
## Установка на новую cron-машину
```bash
# 1. yt-dlp должен быть в PATH (включая cron-окружение)
sudo dnf install -y yt-dlp # или: pip install --user yt-dlp / pipx install yt-dlp
# 2. Залогиниться в Firefox на youtube.com и instagram.com
# 3. Прописать актуальные REMOTE_HOST/DEV_HOST и пути в update_cookies.sh
# 4. Добавить в crontab
*/20 * * * * /bin/bash -c '/path/to/cookies-cron/update_cookies.sh' >> /path/to/cookies-cron/cron_error.log 2>&1
```
На принимающей стороне (прод и тестовый стенд) cookies монтируются в контейнеры с хоста — см. `docker-compose.prod.yml`/`docker-compose.yml` и раздел «Cookies на проде» в корневом `README.md`. Перезапуск контейнеров после доставки не требуется — `yt-dlp` читает файл с диска при каждом запросе.

View file

@ -1,76 +0,0 @@
#!/bin/bash
# Скрипт для получения cookies Instagram через yt-dlp из Firefox
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
COOKIES_FILE="$SCRIPT_DIR/instagram_cookies.txt"
BROWSER="firefox"
# Если запущено с sudo, используем HOME реального пользователя
if [ -n "$SUDO_USER" ]; then
REAL_HOME=$(getent passwd "$SUDO_USER" | cut -d: -f6)
export HOME="$REAL_HOME"
echo "⚠️ Обнаружен sudo, использую домашнюю директорию пользователя: $HOME"
fi
echo "Получение cookies Instagram из Firefox..."
echo "Файл cookies будет сохранен в: $COOKIES_FILE"
echo "Получаю cookies..."
# Извлекаем cookies из браузера напрямую из базы данных (быстро)
# --cookies-from-browser извлекает все cookies для домена
# Используем один запрос - все cookies для instagram.com будут извлечены
echo "Получаю cookies из браузера..."
timeout 10 yt-dlp \
--cookies-from-browser "$BROWSER" \
--cookies "$COOKIES_FILE" \
--no-download \
--quiet \
--socket-timeout 5 \
"https://www.instagram.com" 2>&1 | head -20 || true
# Проверяем, был ли создан файл cookies (не проверяем EXIT_CODE, т.к. команды могут завершиться с ошибками, но cookies могут быть получены)
if [ -f "$COOKIES_FILE" ]; then
# Проверяем, что файл содержит данные (не только заголовки)
COOKIE_LINES=$(grep -v '^#' "$COOKIES_FILE" | grep -v '^$' | wc -l)
if [ "$COOKIE_LINES" -gt 0 ]; then
echo ""
echo "✅ Cookies успешно сохранены в $COOKIES_FILE"
echo " Найдено строк с cookies: $COOKIE_LINES"
echo ""
echo "Теперь перезапустите instagram-downloader:"
echo " docker compose -f instagram-downloader/docker-compose.yml restart"
echo ""
echo "Или перезапустите все сервисы:"
echo " ./stop_all.sh && ./start_all.sh"
else
echo ""
echo "❌ Ошибка: файл cookies создан, но не содержит данных"
rm -f "$COOKIES_FILE"
exit 1
fi
else
echo ""
echo "❌ Ошибка: файл cookies не был создан"
echo ""
echo "Возможные причины:"
echo "1. Firefox не найден или недоступен"
echo "2. Проблемы с правами доступа к файлу cookies браузера"
echo "3. Cookies не найдены в браузере"
echo ""
echo "Проверка:"
if [ -n "$SUDO_USER" ]; then
REAL_HOME=$(getent passwd "$SUDO_USER" | cut -d: -f6)
FIREFOX_DIR="$REAL_HOME/.mozilla/firefox"
else
FIREFOX_DIR="$HOME/.mozilla/firefox"
fi
if [ -d "$FIREFOX_DIR" ]; then
echo " ✓ Директория Firefox найдена: $FIREFOX_DIR"
else
echo " ✗ Директория Firefox не найдена: $FIREFOX_DIR"
echo " Попробуйте запустить скрипт БЕЗ sudo: ./get_instagram_cookies.sh"
fi
exit 1
fi

View file

@ -1,217 +0,0 @@
#!/bin/bash
# Скрипт для получения cookies YouTube через yt-dlp из Firefox
# Предназначен для запуска на Ubuntu 24.04 с авторизованным Firefox
# Результат копируется через cron на серверы со службами скачивания
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
COOKIES_FILE="$SCRIPT_DIR/youtube_cookies.txt"
BROWSER="firefox"
TEMP_FILE="${COOKIES_FILE}.tmp"
# Определяем HOME пользователя (работает для sudo и обычного запуска)
if [ -n "$SUDO_USER" ]; then
REAL_HOME=$(getent passwd "$SUDO_USER" | cut -d: -f6)
export HOME="$REAL_HOME"
elif [ -z "$HOME" ] || [ "$HOME" = "/root" ]; then
# Если HOME не установлен или это root, пытаемся найти реального пользователя
REAL_USER=$(whoami)
if [ "$REAL_USER" != "root" ]; then
export HOME=$(getent passwd "$REAL_USER" | cut -d: -f6)
fi
fi
# Проверка для cron: если запущен без терминала, меньше вывода
if [ -t 0 ]; then
VERBOSE=1
else
VERBOSE=0
fi
if [ "$VERBOSE" = "1" ]; then
echo "Получение cookies YouTube из Firefox..."
echo "Файл cookies будет сохранен в: $COOKIES_FILE"
fi
# Проверяем наличие yt-dlp
if ! command -v yt-dlp &> /dev/null; then
echo "ERROR: yt-dlp не найден в системе" >&2
echo "Установите: sudo apt install yt-dlp (Ubuntu) или pip install yt-dlp" >&2
exit 1
fi
if [ "$VERBOSE" = "1" ]; then
echo "Получаю cookies..."
fi
# Удаляем старый временный файл
rm -f "$TEMP_FILE"
# Извлекаем cookies из браузера напрямую из базы данных
# --cookies-from-browser извлекает cookies для всех доменов, связанных с URL
# Используем один запрос с основным URL - cookies для всех поддоменов будут извлечены
if [ "$VERBOSE" = "1" ]; then
echo "Получаю cookies из браузера..."
timeout 10 yt-dlp \
--cookies-from-browser "$BROWSER" \
--cookies "$TEMP_FILE" \
--no-download \
--quiet \
--socket-timeout 5 \
"https://www.youtube.com" > /dev/null 2>&1 || true
else
timeout 10 yt-dlp \
--cookies-from-browser "$BROWSER" \
--cookies "$TEMP_FILE" \
--no-download \
--quiet \
--socket-timeout 5 \
"https://www.youtube.com" > /dev/null 2>&1 || true
fi
# Функция проверки валидности куков
check_cookies() {
local file="$1"
local current_time=$(date +%s)
# Проверяем существование файла
if [ ! -f "$file" ]; then
return 1
fi
# Проверяем, что файл содержит данные (не только заголовки)
local cookie_lines=$(grep -v '^#' "$file" | grep -v '^$' | wc -l)
if [ "$cookie_lines" -eq 0 ]; then
return 1
fi
# Проверяем наличие YouTube-куков
local youtube_cookies=$(grep -E '\.youtube\.com|youtube\.com' "$file" | grep -v '^#' | wc -l)
if [ "$youtube_cookies" -eq 0 ]; then
return 1
fi
# Проверяем срок действия куков (должен быть хотя бы один не просроченный)
local valid_cookies=0
local expired_cookies=0
# Используем awk для более надежного парсинга
while IFS=$'\t' read -r domain flag path secure expiration name value; do
# Пропускаем комментарии и пустые строки
[[ "$domain" =~ ^#.*$ ]] && continue
[[ -z "$domain" ]] && continue
# Проверяем только YouTube-куки
if [[ "$domain" =~ youtube\.com ]]; then
if [ "$expiration" = "0" ] || [ -z "$expiration" ]; then
# Сессионные куки (expiration=0) считаем валидными
valid_cookies=$((valid_cookies + 1))
else
# Проверяем срок действия
if [ "$expiration" -gt "$current_time" ] 2>/dev/null; then
valid_cookies=$((valid_cookies + 1))
else
expired_cookies=$((expired_cookies + 1))
fi
fi
fi
done < "$file"
# Должен быть хотя бы один валидный YouTube-куки
if [ "$valid_cookies" -eq 0 ]; then
return 1
fi
# Если есть просроченные куки, выводим предупреждение, но не считаем критичным
if [ "$expired_cookies" -gt 0 ] && [ "$VERBOSE" = "1" ]; then
echo "⚠️ Предупреждение: найдено $expired_cookies просроченных YouTube-куков" >&2
fi
return 0
}
# Проверяем результат
if [ -f "$TEMP_FILE" ] && check_cookies "$TEMP_FILE"; then
# Куки валидны, переносим во временный файл в финальный
mv "$TEMP_FILE" "$COOKIES_FILE"
# Собираем статистику для вывода
COOKIE_LINES=$(grep -v '^#' "$COOKIES_FILE" | grep -v '^$' | wc -l)
YOUTUBE_COOKIES=$(grep -E '\.youtube\.com|youtube\.com' "$COOKIES_FILE" | grep -v '^#' | wc -l)
IMPORTANT_COOKIES=$(grep -E 'VISITOR_INFO1_LIVE|__Secure-3PSID|PREF|__Secure-YNID' "$COOKIES_FILE" | grep -v '^#' | wc -l)
if [ "$VERBOSE" = "1" ]; then
echo ""
echo "✅ Cookies успешно сохранены в $COOKIES_FILE"
echo " Всего строк с cookies: $COOKIE_LINES"
echo " YouTube-куков: $YOUTUBE_COOKIES"
echo " Важных куков: $IMPORTANT_COOKIES"
echo ""
echo "Файл готов к копированию на серверы через cron"
else
# Минимальный вывод для cron (можно логировать в файл)
echo "$(date '+%Y-%m-%d %H:%M:%S') - YouTube cookies updated: $YOUTUBE_COOKIES cookies, $IMPORTANT_COOKIES important"
fi
exit 0
else
# Очищаем временный файл при ошибке
rm -f "$TEMP_FILE"
if [ "$VERBOSE" = "1" ]; then
echo ""
echo "❌ Ошибка: не удалось получить валидные YouTube-куки"
echo ""
echo "Возможные причины:"
echo "1. В Firefox нет куков для YouTube"
echo "2. Откройте YouTube в Firefox и войдите в аккаунт"
echo "3. Все куки просрочены - обновите их в браузере"
echo ""
else
echo "$(date '+%Y-%m-%d %H:%M:%S') - ERROR: Failed to get valid YouTube cookies" >&2
fi
if [ ! -f "$COOKIES_FILE" ]; then
# Если файла не было, это критичная ошибка
exit 1
else
# Если файл был, просто оставляем старый (лучше работать со старыми куками чем без них)
if [ "$VERBOSE" = "1" ]; then
echo "⚠️ Оставляю существующий файл cookies без изменений"
fi
exit 0
fi
fi
if [ ! -f "$TEMP_FILE" ] && [ ! -f "$COOKIES_FILE" ]; then
if [ "$VERBOSE" = "1" ]; then
echo ""
echo "❌ Ошибка: файл cookies не был создан"
echo ""
echo "Возможные причины:"
echo "1. Firefox не найден или недоступен"
echo "2. Проблемы с правами доступа к файлу cookies браузера"
echo "3. Cookies не найдены в браузере"
echo ""
echo "Проверка:"
FIREFOX_DIR="$HOME/.mozilla/firefox"
if [ -d "$FIREFOX_DIR" ]; then
echo " ✓ Директория Firefox найдена: $FIREFOX_DIR"
else
echo " ✗ Директория Firefox не найдена: $FIREFOX_DIR"
echo " Убедитесь что Firefox установлен и вы авторизованы на YouTube"
fi
else
echo "$(date '+%Y-%m-%d %H:%M:%S') - ERROR: Cookies file was not created" >&2
fi
# Если был старый файл, оставляем его
if [ -f "$COOKIES_FILE" ]; then
if [ "$VERBOSE" = "1" ]; then
echo "⚠️ Оставляю существующий файл cookies без изменений"
fi
exit 0
else
exit 1
fi
fi

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 восстановлены`.

View file

@ -1,6 +1,10 @@
#!/bin/bash
# Скрипт для обновления cookies Instagram и YouTube через cron
# Запускает скрипты получения cookies и копирует их на удаленный хост
# Доставка cookies на прод и тестовый стенд.
#
# Запускается ВРУЧНУЮ после разового экспорта cookies из браузера — не по крону.
# Как правильно экспортировать cookies, см. README.md рядом с этим скриптом.
# Автоматический экспорт через `yt-dlp --cookies-from-browser` убран намеренно:
# он ломает YouTube-сессию ("cookies have likely been rotated in the browser").
set -euo pipefail
@ -9,8 +13,6 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
# Настройки
INSTAGRAM_SCRIPT="$SCRIPT_DIR/get_instagram_cookies.sh"
YOUTUBE_SCRIPT="$SCRIPT_DIR/get_youtube_cookies.sh"
INSTAGRAM_COOKIES="$SCRIPT_DIR/instagram_cookies.txt"
YOUTUBE_COOKIES="$SCRIPT_DIR/youtube_cookies.txt"
REMOTE_HOST="germany"
@ -19,7 +21,7 @@ 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/cron.log"
LOG_FILE="$SCRIPT_DIR/deliver.log"
# Функция логирования
log() {
@ -32,48 +34,28 @@ error_exit() {
exit 1
}
log "=== Начало обновления cookies ==="
# Проверяет, что файл существует и содержит хотя бы одну строку с cookie
check_cookies_file() {
local file="$1"
local name="$2"
# Проверяем наличие скриптов
if [ ! -f "$INSTAGRAM_SCRIPT" ]; then
error_exit "Скрипт не найден: $INSTAGRAM_SCRIPT"
if [ ! -f "$file" ]; then
error_exit "Файл не найден: $file. Сначала экспортируйте cookies $name — см. README.md"
fi
if [ ! -f "$YOUTUBE_SCRIPT" ]; then
error_exit "Скрипт не найден: $YOUTUBE_SCRIPT"
local lines
lines=$(grep -cv -e '^#' -e '^$' "$file" || true)
if [ "$lines" -eq 0 ]; then
error_exit "Файл $file пустой (0 строк с cookies). Экспорт $name не удался — см. README.md"
fi
# Проверяем, что скрипты исполняемые
chmod +x "$INSTAGRAM_SCRIPT" "$YOUTUBE_SCRIPT"
log "$name: $file ($lines строк с cookies)"
}
# Получаем cookies Instagram
log "Получение cookies Instagram..."
if bash "$INSTAGRAM_SCRIPT" >> "$LOG_FILE" 2>&1; then
if [ -f "$INSTAGRAM_COOKIES" ]; then
log "✅ Cookies Instagram успешно получены"
else
error_exit "Файл cookies Instagram не был создан"
fi
else
error_exit "Ошибка при получении cookies Instagram (код возврата: $?)"
fi
log "=== Начало доставки cookies ==="
# Получаем cookies YouTube
log "Получение cookies YouTube..."
if bash "$YOUTUBE_SCRIPT" >> "$LOG_FILE" 2>&1; then
if [ -f "$YOUTUBE_COOKIES" ]; then
log "✅ Cookies YouTube успешно получены"
else
error_exit "Файл cookies YouTube не был создан"
fi
else
# YouTube скрипт может завершиться с кодом 0, даже если новый файл не создан (использует старый)
if [ -f "$YOUTUBE_COOKIES" ]; then
log "✅ Cookies YouTube (возможно использован старый файл)"
else
error_exit "Файл cookies YouTube не найден"
fi
fi
check_cookies_file "$INSTAGRAM_COOKIES" "Instagram"
check_cookies_file "$YOUTUBE_COOKIES" "YouTube"
# Копируем cookies на удаленный хост (прод)
log "Копирование cookies на удаленный хост $REMOTE_HOST..."
@ -107,5 +89,6 @@ else
log "⚠️ Не удалось скопировать YouTube cookies на $DEV_HOST (продолжаем)"
fi
log "=== Обновление cookies завершено успешно ==="
log "=== Доставка cookies завершена успешно ==="
log "Проверить результат: ssh ${REMOTE_HOST} \"curl -s -X POST localhost:5557/cookies/check; curl -s -X POST localhost:5556/cookies/check\""
exit 0

View file

@ -1,51 +1,7 @@
# Инструкция по получению cookies для Instagram
## Вариант 1: Использование расширения браузера (рекомендуется)
Актуальная инструкция живёт в одном месте — [`cookies/README.md`](../cookies/README.md).
1. Установите расширение для экспорта cookies:
- Chrome: [Get cookies.txt LOCALLY](https://chrome.google.com/webstore/detail/get-cookiestxt-locally/cclelndahbckbenkjhflpdbgdldlbecc)
- Firefox: [cookies.txt](https://addons.mozilla.org/en-US/firefox/addon/cookies-txt/)
2. Откройте Instagram и войдите в свой аккаунт: https://www.instagram.com
3. Кликните на расширение и выберите "Export cookies.txt"
4. Сохраните файл как `instagram_cookies.txt` в корень проекта (там же, где docker-compose.yml)
## Вариант 2: Ручной экспорт через DevTools
1. Откройте Instagram в браузере и войдите: https://www.instagram.com
2. Откройте DevTools (F12) → вкладка Application/Storage → Cookies → https://www.instagram.com
3. Скопируйте нужные cookies в формате Netscape:
```
# Netscape HTTP Cookie File
.instagram.com TRUE / FALSE 1735689600 sessionid YOUR_SESSION_ID
.instagram.com TRUE / FALSE 1735689600 csrftoken YOUR_CSRF_TOKEN
```
4. Сохраните в файл `instagram_cookies.txt`
## Вариант 3: Использование yt-dlp для экспорта
```bash
# Экспорт cookies из браузера Chrome
yt-dlp --cookies-from-browser chrome --cookies instagram_cookies.txt https://www.instagram.com
# Или из Firefox
yt-dlp --cookies-from-browser firefox --cookies instagram_cookies.txt https://www.instagram.com
```
## Важно!
- Файл должен называться `instagram_cookies.txt`
- Разместите его в корне проекта (рядом с docker-compose.yml)
- Cookies имеют срок действия - возможно, потребуется обновлять их периодически
- Не коммитьте файл в git (он уже добавлен в .gitignore)
После добавления файла перезапустите контейнер:
```bash
docker compose restart bot
```
Там описано: когда обновлять (по алерту `🍪⚠️ ПРОАКТИВНАЯ ПРОВЕРКА COOKIES` из админ-бота, а не по расписанию), как правильно экспортировать куки расширением для `cookies.txt` и как доставить их на прод через `deliver_cookies.sh`.
**Не используйте `yt-dlp --cookies-from-browser ... --cookies file.txt`** — этот способ раньше был описан здесь, но wiki yt-dlp его прямо запрещает: он ломает сессию, и куки протухают («cookies have likely been rotated in the browser as a security measure»).

View file

@ -11,27 +11,9 @@
### 1. Настройка cookies
Перед запуском сервиса необходимо получить cookies Instagram. Есть несколько способов:
Перед запуском сервиса нужно положить `instagram_cookies.txt` в папку `instagram-downloader/`.
#### Способ 1: Через скрипт (рекомендуется)
```bash
cd instagram-downloader
./get_instagram_cookies.sh
```
Скрипт попросит выбрать браузер и автоматически извлечет cookies.
#### Способ 2: Обновление существующих cookies
```bash
cd instagram-downloader
./update_instagram_cookies.sh
```
#### Способ 3: Вручную
См. подробные инструкции в `INSTAGRAM_COOKIES_INSTRUCTIONS.md`
Куки экспортируются вручную из браузера расширением для `cookies.txt` — пошаговая процедура в [`cookies/README.md`](../cookies/README.md). Скрипты автоматического экспорта через `yt-dlp --cookies-from-browser` убраны: этот способ ломает сессию, куки после него быстро протухают.
### 2. Запуск сервиса
@ -71,6 +53,21 @@ curl http://localhost:5556/health
}
```
### POST /cookies/check
Проверка, работают ли cookies прямо сейчас: делает лёгкий `extract_info` (без скачивания) по публичному тестовому ролику. Используется фоновым мониторингом в `bot.py`.
**Ответ:**
```json
{
"cookies_present": true,
"cookies_valid": false,
"detail": "текст ошибки от yt-dlp"
}
```
`cookies_valid: null` означает, что файл с куками не найден. Тестовый URL переопределяется переменной `INSTAGRAM_COOKIE_TEST_URL`.
### POST /download/stream
Скачивание видео с Instagram.
@ -93,40 +90,22 @@ curl http://localhost:5556/health
## Обновление cookies
Cookies Instagram имеют ограниченный срок действия. Рекомендуется обновлять их раз в несколько недель.
Ключевые куки Instagram живут около года, поэтому обновлять их по расписанию не нужно. Триггер на обновление — алерт `🍪⚠️ ПРОАКТИВНАЯ ПРОВЕРКА COOKIES` из админ-бота (за состоянием следит фоновая проверка в `bot.py`, которая раз в 30 минут дёргает `POST /cookies/check`).
Для обновления:
```bash
cd instagram-downloader
./update_instagram_cookies.sh
```
После обновления перезапустите сервис:
```bash
docker compose restart instagram-downloader
```
Процедура экспорта и доставки — в [`cookies/README.md`](../cookies/README.md). Перезапускать сервис после обновления не нужно: `yt-dlp` читает файл с диска при каждом запросе.
## Troubleshooting
### Сервис не может скачать видео
1. Проверьте наличие файла `instagram_cookies.txt` в папке `instagram-downloader/`
2. Проверьте срок действия cookies (они могут истечь)
3. Обновите cookies через скрипт `update_instagram_cookies.sh`
1. Проверьте состояние куков: `curl -s -X POST localhost:5556/cookies/check`
2. Проверьте наличие файла `instagram_cookies.txt` в папке `instagram-downloader/` и что в нём есть `sessionid`
3. Если `cookies_valid: false` — обновите куки по инструкции в [`cookies/README.md`](../cookies/README.md)
4. Проверьте логи: `docker compose logs instagram-downloader`
### Cookies истекли
### Cookies есть и не истекли по сроку, но всё равно `login required`
Если видите ошибку "Instagram cookies истекли", выполните:
```bash
cd instagram-downloader
./update_instagram_cookies.sh
```
Затем перезапустите сервис.
Instagram умеет блокировать сессию на своей стороне — тогда в логах видно `Main webpage is locked behind the login page` при формально валидном `sessionid`. Лечится только повторным экспортом куков. Провоцирует такую блокировку активная автоматическая работа с аккаунтом (например, постоянно открытая вкладка Instagram, непрерывно ходящая в сеть), поэтому после экспорта браузер лучше закрывать.
## Структура файлов
@ -136,9 +115,7 @@ instagram-downloader/
├── Dockerfile # Образ Docker
├── docker-compose.yml # Конфигурация для отдельного запуска
├── requirements.txt # Python зависимости
├── get_instagram_cookies.sh # Скрипт для получения cookies
├── update_instagram_cookies.sh # Скрипт для обновления cookies
├── INSTAGRAM_COOKIES_INSTRUCTIONS.md # Подробные инструкции по cookies
├── INSTAGRAM_COOKIES_INSTRUCTIONS.md # Отсылка к cookies/README.md
└── README.md # Этот файл
```

View file

@ -206,12 +206,24 @@ INSTAGRAM_COOKIE_TEST_URL = os.getenv(
)
# Маркеры, по которым сбой однозначно относится к cookies/авторизации.
# Всё остальное считаем поломкой извлечения (версия yt-dlp, изменения на стороне
# Instagram) и про cookies ничего не утверждаем — иначе мониторинг уводит
# диагностику не туда: например, "empty media response" воспроизводится и без cookies.
INSTAGRAM_COOKIE_INVALID_MARKERS = (
'login required',
'locked behind the login page',
'rate-limit reached',
)
@app.route('/cookies/check', methods=['POST'])
def cookies_check():
"""Проверяет, рабочие ли сейчас Instagram cookies (для проактивного мониторинга)."""
cookies_file = Path(os.getenv('INSTAGRAM_COOKIES_FILE', 'instagram_cookies.txt'))
if not cookies_file.exists():
return jsonify({'cookies_present': False, 'cookies_valid': None,
'status': 'no_cookies',
'detail': 'cookies file missing'}), 200
ydl_opts = {
@ -224,10 +236,16 @@ def cookies_check():
try:
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
ydl.extract_info(INSTAGRAM_COOKIE_TEST_URL, download=False)
return jsonify({'cookies_present': True, 'cookies_valid': True, 'detail': ''}), 200
return jsonify({'cookies_present': True, 'cookies_valid': True,
'status': 'ok', 'detail': ''}), 200
except Exception as e:
err = str(e)
detail = err[-500:]
if any(marker in err.lower() for marker in INSTAGRAM_COOKIE_INVALID_MARKERS):
return jsonify({'cookies_present': True, 'cookies_valid': False,
'detail': str(e)[-500:]}), 200
'status': 'cookies_invalid', 'detail': detail}), 200
return jsonify({'cookies_present': True, 'cookies_valid': None,
'status': 'extraction_failed', 'detail': detail}), 200
@app.route('/download/stream', methods=['POST'])

View file

@ -1,79 +0,0 @@
#!/bin/bash
# Скрипт для получения cookies Instagram через yt-dlp из Firefox
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
COOKIES_FILE="$SCRIPT_DIR/instagram_cookies.txt"
BROWSER="firefox"
# Если запущено с sudo, используем HOME реального пользователя
if [ -n "$SUDO_USER" ]; then
REAL_HOME=$(getent passwd "$SUDO_USER" | cut -d: -f6)
export HOME="$REAL_HOME"
echo "⚠️ Обнаружен sudo, использую домашнюю директорию пользователя: $HOME"
fi
echo "Получение cookies Instagram из Firefox..."
echo "Файл cookies будет сохранен в: $COOKIES_FILE"
echo "Получаю cookies..."
# Используем таймаут для предотвращения зависания
timeout 15 yt-dlp \
--cookies-from-browser "$BROWSER" \
--cookies "$COOKIES_FILE" \
--no-download \
--quiet \
"https://www.instagram.com" 2>&1 | head -20
EXIT_CODE=$?
if [ $EXIT_CODE -eq 124 ]; then
echo ""
echo "⚠️ Процесс получения cookies превысил таймаут (15 сек)"
echo " Проверяю, был ли создан файл cookies..."
elif [ $EXIT_CODE -ne 0 ] && [ $EXIT_CODE -ne 124 ]; then
echo ""
echo "⚠️ Процесс завершился с кодом $EXIT_CODE"
echo " Проверяю, был ли создан файл cookies..."
fi
if [ -f "$COOKIES_FILE" ]; then
# Проверяем, что файл содержит данные (не только заголовки)
COOKIE_LINES=$(grep -v '^#' "$COOKIES_FILE" | grep -v '^$' | wc -l)
if [ "$COOKIE_LINES" -gt 0 ]; then
echo ""
echo "✅ Cookies успешно сохранены в $COOKIES_FILE"
echo " Найдено строк с cookies: $COOKIE_LINES"
echo ""
echo "Если cookies смонтированы с хоста (см. docker-compose.yml/docker-compose.prod.yml),"
echo "перезапуск не нужен — yt-dlp читает файл заново при каждом запросе."
else
echo ""
echo "❌ Ошибка: файл cookies создан, но не содержит данных"
rm -f "$COOKIES_FILE"
exit 1
fi
else
echo ""
echo "❌ Ошибка: файл cookies не был создан"
echo ""
echo "Возможные причины:"
echo "1. Firefox не найден или недоступен"
echo "2. Проблемы с правами доступа к файлу cookies браузера"
echo "3. Cookies не найдены в браузере"
echo ""
echo "Проверка:"
if [ -n "$SUDO_USER" ]; then
REAL_HOME=$(getent passwd "$SUDO_USER" | cut -d: -f6)
FIREFOX_DIR="$REAL_HOME/.mozilla/firefox"
else
FIREFOX_DIR="$HOME/.mozilla/firefox"
fi
if [ -d "$FIREFOX_DIR" ]; then
echo " ✓ Директория Firefox найдена: $FIREFOX_DIR"
else
echo " ✗ Директория Firefox не найдена: $FIREFOX_DIR"
echo " Попробуйте запустить скрипт БЕЗ sudo: ./get_instagram_cookies.sh"
fi
exit 1
fi

View file

@ -1,5 +1,5 @@
Flask==3.0.0
flask-cors==4.0.0
yt-dlp>=2024.12.13
yt-dlp>=2026.7.4
gunicorn==21.2.0

View file

@ -1,47 +0,0 @@
#!/bin/bash
# Обновление cookies Instagram через браузер
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
COOKIES_FILE="$SCRIPT_DIR/instagram_cookies.txt"
echo "Обновление cookies Instagram..."
echo ""
echo "Выберите браузер:"
echo "1) Chrome"
echo "2) Firefox"
echo "3) Edge"
echo "4) Opera"
read -p "Введите номер (1-4): " browser
case "$browser" in
1) BROWSER="chrome" ;;
2) BROWSER="firefox" ;;
3) BROWSER="edge" ;;
4) BROWSER="opera" ;;
*)
echo "Неверный выбор"
exit 1
;;
esac
echo ""
echo "ВАЖНО: Перед обновлением cookies убедитесь, что вы авторизованы в Instagram в выбранном браузере!"
echo ""
read -p "Нажмите Enter для продолжения или Ctrl+C для отмены..."
echo "Обновляю cookies из $BROWSER..."
echo "Файл cookies будет сохранен в: $COOKIES_FILE"
yt-dlp --cookies-from-browser "$BROWSER" --cookies "$COOKIES_FILE" --no-download https://www.instagram.com 2>&1 | head -10
if [ -f "$COOKIES_FILE" ]; then
echo ""
echo "✅ Cookies успешно обновлены в $COOKIES_FILE"
echo ""
echo "Для применения изменений перезапустите instagram-downloader сервис:"
echo " cd $(cd "$SCRIPT_DIR/.." && pwd) && docker compose restart instagram-downloader"
else
echo ""
echo "❌ Ошибка: файл cookies не был создан"
exit 1
fi

128
smoke_test.py Executable file
View file

@ -0,0 +1,128 @@
#!/usr/bin/env python3
"""Смоук-тест загрузчиков: гоняет реальные ссылки через запущенные сервисы.
Запускать после КАЖДОЙ правки, до коммита:
python3 smoke_test.py
Ссылки подобраны не случайно это реальные регрессии, которые уже ломались:
* возрастное YouTube-видео ловит отсутствие рабочего JS-рантайма (n-challenge);
* Instagram-ролик ловит устаревший экстрактор yt-dlp;
* обычное видео проверяет, что базовый путь скачивания цел.
Зависимостей нет только стандартная библиотека, чтобы запускалось на любом хосте.
Код возврата: 0 все проверки прошли, 1 есть падения.
"""
import json
import os
import sys
import time
import urllib.error
import urllib.request
YOUTUBE_URL = os.getenv('YOUTUBE_DOWNLOADER_URL', 'http://localhost:5557')
INSTAGRAM_URL = os.getenv('INSTAGRAM_DOWNLOADER_URL', 'http://localhost:5556')
YT_SMALL = 'https://www.youtube.com/watch?v=jNQXAC9IVRw' # «Me at the zoo», 19 секунд
YT_AGE_RESTRICTED = 'https://youtu.be/UMyoCr2MnpM' # требует cookies + решения n-challenge
IG_REEL = 'https://www.instagram.com/reel/Dak0fDTMv_i/' # обычный публичный reel
MIN_VIDEO_BYTES = 100 * 1024 # меньше — значит пришло не видео, а заглушка/ошибка
def _post(url, payload=None, timeout=120):
"""POST с JSON-телом. Возвращает (http_code, body_bytes)."""
data = json.dumps(payload or {}).encode()
req = urllib.request.Request(
url, data=data, headers={'Content-Type': 'application/json'}, method='POST'
)
try:
with urllib.request.urlopen(req, timeout=timeout) as resp:
return resp.status, resp.read()
except urllib.error.HTTPError as e:
return e.code, e.read()
except Exception as e:
return None, str(e).encode()
def check_cookies(service, base_url):
"""/cookies/check должен вернуть status=ok."""
code, body = _post(f'{base_url}/cookies/check', timeout=180)
if code != 200:
return False, f'HTTP {code}'
try:
data = json.loads(body)
except ValueError:
return False, 'ответ не JSON'
status = data.get('status', '?')
if status == 'ok':
return True, 'status=ok'
return False, f"status={status}: {(data.get('detail') or '')[:120]}"
def check_formats(url):
"""/formats у YouTube — лёгкая проверка: cookies + JS-рантайм без скачивания."""
code, body = _post(f'{YOUTUBE_URL}/formats', {'url': url}, timeout=180)
if code != 200:
detail = body.decode('utf-8', 'replace')[:150]
return False, f'HTTP {code}: {detail}'
try:
formats = json.loads(body).get('formats', [])
except ValueError:
return False, 'ответ не JSON'
if not formats:
return False, 'форматы не найдены'
return True, f'форматов: {len(formats)}'
def check_download(base_url, url):
"""/download/stream — полный путь до реального файла."""
code, body = _post(f'{base_url}/download/stream', {'url': url}, timeout=600)
if code != 200:
detail = body.decode('utf-8', 'replace')[:150]
return False, f'HTTP {code}: {detail}'
size = len(body)
if size < MIN_VIDEO_BYTES:
return False, f'подозрительно мало: {size} байт'
return True, f'{size / 1024:.0f} КБ'
CHECKS = [
('cookies YouTube', lambda: check_cookies('youtube', YOUTUBE_URL)),
('cookies Instagram', lambda: check_cookies('instagram', INSTAGRAM_URL)),
('YouTube возрастное', lambda: check_formats(YT_AGE_RESTRICTED)),
('YouTube скачивание', lambda: check_download(YOUTUBE_URL, YT_SMALL)),
('Instagram скачивание', lambda: check_download(INSTAGRAM_URL, IG_REEL)),
]
def main():
print(f'Смоук-тест: {YOUTUBE_URL} / {INSTAGRAM_URL}\n')
results = []
for name, fn in CHECKS:
started = time.monotonic()
try:
ok, detail = fn()
except Exception as e: # проверка не должна ронять весь прогон
ok, detail = False, f'исключение: {e}'
results.append((name, ok, detail, time.monotonic() - started))
width = max(len(name) for name, *_ in results)
print(f'| {"Проверка".ljust(width)} | Итог | Время | Детали')
print(f'|{"-" * (width + 2)}|------|-------|--------')
for name, ok, detail, elapsed in results:
mark = ' OK ' if ok else 'FAIL'
print(f'| {name.ljust(width)} | {mark} | {elapsed:5.1f}с | {detail}')
failed = [name for name, ok, *_ in results if not ok]
print()
if failed:
print(f'ИТОГ: провалено {len(failed)} из {len(results)}{", ".join(failed)}')
return 1
print(f'ИТОГ: все проверки прошли ({len(results)} из {len(results)})')
return 0
if __name__ == '__main__':
sys.exit(main())

View file

@ -1,5 +1,20 @@
FROM python:3.11-slim
RUN apt-get update && apt-get install -y ffmpeg wget curl aria2 nodejs npm && rm -rf /var/lib/apt/lists/*
RUN apt-get update && apt-get install -y ffmpeg wget curl aria2 unzip && rm -rf /var/lib/apt/lists/*
# Deno — рекомендованный yt-dlp рантайм для решения n-challenge YouTube.
# Node из Debian bookworm — версия 20, а yt-dlp требует минимум 22 (иначе
# "JS runtimes: node-20.x (unsupported)" и "No video formats found" на путях с cookies).
RUN set -eux; \
case "$(uname -m)" in \
x86_64) DENO_ARCH=x86_64-unknown-linux-gnu ;; \
aarch64) DENO_ARCH=aarch64-unknown-linux-gnu ;; \
*) echo "unsupported arch: $(uname -m)" >&2; exit 1 ;; \
esac; \
curl -fsSL --retry 5 --retry-all-errors -o /tmp/deno.zip \
"https://github.com/denoland/deno/releases/latest/download/deno-${DENO_ARCH}.zip"; \
unzip -q /tmp/deno.zip -d /usr/local/bin; \
rm /tmp/deno.zip; \
chmod +x /usr/local/bin/deno; \
deno --version
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

View file

@ -143,7 +143,7 @@ def _build_ytdlp_base_cmd() -> list:
YTDLP_CMD,
'--socket-timeout', '15',
'--extractor-args', EXTRACTOR_ARGS,
'--js-runtimes', 'node',
'--js-runtimes', 'deno',
'--remote-components', 'ejs:github',
'--no-playlist',
]
@ -490,7 +490,14 @@ YOUTUBE_COOKIE_TEST_URL = os.getenv(
'YOUTUBE_COOKIE_TEST_URL',
'https://www.youtube.com/watch?v=jNQXAC9IVRw' # "Me at the zoo" — первое видео на YouTube, публичное, без возрастных ограничений
)
YOUTUBE_COOKIE_INVALID_MARKERS = ('no longer valid', 'have likely been rotated')
# Маркеры, по которым сбой однозначно относится к cookies/авторизации.
# Всё остальное считаем поломкой извлечения (версия yt-dlp, JS-рантайм, сеть) и
# про cookies ничего не утверждаем — иначе мониторинг уводит диагностику не туда.
YOUTUBE_COOKIE_INVALID_MARKERS = (
'no longer valid',
'have likely been rotated',
'sign in to confirm',
)
@app.route('/cookies/check', methods=['POST'])
@ -499,6 +506,7 @@ def cookies_check():
cookies_file = Path(os.getenv('YOUTUBE_COOKIES_FILE', '/app/youtube_cookies.txt'))
if not (cookies_file.exists() and cookies_file.stat().st_size > 0):
return jsonify({'cookies_present': False, 'cookies_valid': None,
'status': 'no_cookies',
'detail': 'cookies file missing or empty'}), 200
# Без --quiet/--no-warnings: сигнал ротации кук — это подавляемый WARNING, а не ошибка с ненулевым returncode
@ -506,15 +514,23 @@ def cookies_check():
try:
result = _run_ytdlp(cmd, timeout=INFO_TIMEOUT)
except Exception as e:
return jsonify({'cookies_present': True, 'cookies_valid': False,
return jsonify({'cookies_present': True, 'cookies_valid': None,
'status': 'extraction_failed',
'detail': f'yt-dlp invocation failed: {e}'}), 200
stderr = result.stderr
if any(marker in stderr for marker in YOUTUBE_COOKIE_INVALID_MARKERS) or result.returncode != 0:
return jsonify({'cookies_present': True, 'cookies_valid': False,
'detail': stderr.strip()[-500:]}), 200
detail = stderr.strip()[-500:]
return jsonify({'cookies_present': True, 'cookies_valid': True, 'detail': ''}), 200
if any(marker in stderr.lower() for marker in YOUTUBE_COOKIE_INVALID_MARKERS):
return jsonify({'cookies_present': True, 'cookies_valid': False,
'status': 'cookies_invalid', 'detail': detail}), 200
if result.returncode != 0:
return jsonify({'cookies_present': True, 'cookies_valid': None,
'status': 'extraction_failed', 'detail': detail}), 200
return jsonify({'cookies_present': True, 'cookies_valid': True,
'status': 'ok', 'detail': ''}), 200
@app.route('/download/stream', methods=['POST'])
@ -570,9 +586,10 @@ def download_stream():
error_msg = (
f"{error_str}\n\n"
"💡 Совет: Cookies устарели или недействительны. "
"Обновите cookies через скрипт:\n"
" ./youtube-downloader/get_youtube_cookies.sh\n"
"Затем перезапустите сервис."
"Обновите их вручную по инструкции в cookies/README.md "
"(для YouTube — только через приватное окно, иначе сессия ротируется) "
"и доставьте через cookies/deliver_cookies.sh. "
"Перезапуск сервиса не требуется."
)
else:
error_msg = error_str

View file

@ -1,103 +0,0 @@
#!/bin/bash
# Скрипт для получения cookies YouTube через yt-dlp из Firefox
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
COOKIES_FILE="$SCRIPT_DIR/youtube_cookies.txt"
BROWSER="firefox"
# Если запущено с sudo, используем HOME реального пользователя
if [ -n "$SUDO_USER" ]; then
REAL_HOME=$(getent passwd "$SUDO_USER" | cut -d: -f6)
export HOME="$REAL_HOME"
echo "⚠️ Обнаружен sudo, использую домашнюю директорию пользователя: $HOME"
fi
echo "Получение cookies YouTube из Firefox..."
echo "Файл cookies будет сохранен в: $COOKIES_FILE"
# Проверяем наличие yt-dlp
if ! command -v yt-dlp &> /dev/null; then
echo ""
echo "❌ yt-dlp не найден в системе"
echo ""
echo "Установите yt-dlp одним из способов:"
echo ""
echo "1. Через pacman (Arch Linux):"
echo " sudo pacman -S yt-dlp"
echo ""
echo "2. Через pip:"
echo " pip install yt-dlp"
echo " или"
echo " pip3 install yt-dlp"
echo ""
echo "3. Через pipx (рекомендуется):"
echo " pipx install yt-dlp"
echo ""
exit 1
fi
echo "Получаю cookies..."
# Используем простой способ: извлекаем cookies из браузера и сохраняем в файл
# Используем конкретное короткое видео (не плейлист, не главную страницу)
# Таймаут 10 секунд
# --no-download - не скачивать видео
# --skip-download - пропустить скачивание
# --flat-playlist - не извлекать информацию о видео в плейлистах
# Перенаправляем весь вывод, чтобы не видеть процесс скачивания
timeout 10 yt-dlp \
--cookies-from-browser "$BROWSER" \
--cookies "$COOKIES_FILE" \
--no-download \
--skip-download \
--flat-playlist \
--quiet \
"https://www.youtube.com/watch?v=jNQXAC9IVRw" > /dev/null 2>&1 || true
EXIT_CODE=$?
# Не важно, какой код возврата - главное проверить, создался ли файл cookies
# yt-dlp может вернуть ошибку, но cookies все равно сохранить
if [ -f "$COOKIES_FILE" ]; then
# Проверяем, что файл содержит данные (не только заголовки)
COOKIE_LINES=$(grep -v '^#' "$COOKIES_FILE" | grep -v '^$' | wc -l)
if [ "$COOKIE_LINES" -gt 0 ]; then
echo ""
echo "✅ Cookies успешно сохранены в $COOKIES_FILE"
echo " Найдено строк с cookies: $COOKIE_LINES"
echo ""
echo "Если cookies смонтированы с хоста (см. docker-compose.yml/docker-compose.prod.yml),"
echo "перезапуск не нужен — yt-dlp читает файл заново при каждом запросе."
else
echo ""
echo "❌ Ошибка: файл cookies создан, но не содержит данных"
rm -f "$COOKIES_FILE"
exit 1
fi
else
echo ""
echo "❌ Ошибка: файл cookies не был создан"
echo ""
echo "Возможные причины:"
echo "1. Firefox не найден или недоступен"
echo "2. Проблемы с правами доступа к файлу cookies браузера"
echo "3. Cookies не найдены в браузере"
echo ""
echo "Проверка:"
if [ -n "$SUDO_USER" ]; then
REAL_HOME=$(getent passwd "$SUDO_USER" | cut -d: -f6)
FIREFOX_DIR="$REAL_HOME/.mozilla/firefox"
else
FIREFOX_DIR="$HOME/.mozilla/firefox"
fi
if [ -d "$FIREFOX_DIR" ]; then
echo " ✓ Директория Firefox найдена: $FIREFOX_DIR"
else
echo " ✗ Директория Firefox не найдена: $FIREFOX_DIR"
echo " Попробуйте запустить скрипт БЕЗ sudo: ./get_youtube_cookies.sh"
fi
exit 1
fi

View file

@ -1,5 +1,5 @@
Flask==3.0.0
flask-cors==4.0.0
yt-dlp>=2025.12.01
yt-dlp>=2026.7.4
gunicorn==21.2.0