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

@ -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