fix(downloaders): Deno runtime, свежий yt-dlp, честный health-check, смоук-тест
All checks were successful
CI/CD Pipeline / build-and-deploy (push) Successful in 2m36s
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:
parent
5092d882e5
commit
d597a5e1c5
19 changed files with 373 additions and 733 deletions
|
|
@ -182,7 +182,7 @@ CREATE TABLE stats (
|
||||||
- (YouTube) `./youtube_cookies.txt:/app/youtube_cookies.txt`
|
- (YouTube) `./youtube_cookies.txt:/app/youtube_cookies.txt`
|
||||||
- (Instagram) `./instagram_cookies.txt:/app/instagram_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` (не коммитится)
|
- Токен бота в `.env` (не коммитится)
|
||||||
- Cookies YouTube/Instagram закоммичены в репозиторий как fallback внутри образа; актуальные версии приходят на хост кроном и монтируются поверх (см. `README.md#cookies-на-проде`)
|
- Cookies YouTube/Instagram закоммичены в репозиторий как fallback внутри образа; актуальные версии кладутся на хост вручную и монтируются поверх (см. `README.md#cookies-на-проде` и `cookies/README.md`)
|
||||||
- Сервисы доступны только по указанным URL
|
- Сервисы доступны только по указанным URL
|
||||||
- Можно добавить API key между сервисами
|
- Можно добавить API key между сервисами
|
||||||
|
|
||||||
|
|
|
||||||
23
README.md
23
README.md
|
|
@ -76,14 +76,9 @@ TIKTOK_DOWNLOADER_URL=http://localhost:5559
|
||||||
|
|
||||||
Если планируете скачивать видео с Instagram:
|
Если планируете скачивать видео с 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/`
|
2. Сохраните файл как `instagram_cookies.txt` в папке `instagram-downloader/`
|
||||||
|
|
||||||
```bash
|
|
||||||
cd instagram-downloader
|
|
||||||
./get_instagram_cookies.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Запуск сервисов
|
### 4. Запуск сервисов
|
||||||
|
|
||||||
Все 7 сервисов (бот, admin-бот, 5 загрузчиков) собираются и запускаются одной командой из корня проекта — единый `docker-compose.yml` уже содержит build-контексты для всех подпапок:
|
Все 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` читают файл с диска при каждом запросе — рестарт контейнера не требуется.
|
В проде `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
43
bot.py
|
|
@ -617,8 +617,13 @@ def _save_cookie_state(state: dict):
|
||||||
logger.error(f"Не удалось сохранить состояние cookie-health: {e}")
|
logger.error(f"Не удалось сохранить состояние cookie-health: {e}")
|
||||||
|
|
||||||
|
|
||||||
async def notify_admin_cookie_alert(service: str, healthy: bool, detail: str):
|
async def notify_admin_cookie_alert(service: str, status: str, detail: str):
|
||||||
"""Проактивное уведомление о состоянии cookies (health-check), не связано с ошибкой конкретного пользователя"""
|
"""Проактивное уведомление от health-check, не связано с ошибкой конкретного пользователя.
|
||||||
|
|
||||||
|
status: 'ok' | 'cookies_invalid' | 'extraction_failed'
|
||||||
|
Тексты разные намеренно: сбой извлечения (версия yt-dlp, JS-рантайм) не должен
|
||||||
|
выглядеть как протухшие cookies, иначе разбор уходит не туда.
|
||||||
|
"""
|
||||||
if not ADMIN_BOT_TOKEN:
|
if not ADMIN_BOT_TOKEN:
|
||||||
return
|
return
|
||||||
admin_chat_id = get_admin_chat_id()
|
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)
|
admin_bot = Bot(token=ADMIN_BOT_TOKEN, request=request)
|
||||||
|
|
||||||
if healthy:
|
if status == 'ok':
|
||||||
text = f"✅ Cookies восстановлены\n\n🍪 Сервис: {service}\nCookies снова рабочие."
|
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:
|
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:
|
if len(text) > 4000:
|
||||||
text = text[:4000] + "…"
|
text = text[:4000] + "…"
|
||||||
|
|
||||||
await admin_bot.send_message(chat_id=admin_chat_id, text=text)
|
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:
|
except Exception as e:
|
||||||
logger.error(f"Ошибка при отправке уведомления о cookies админ боту: {e}")
|
logger.error(f"Ошибка при отправке уведомления о cookies админ боту: {e}")
|
||||||
|
|
@ -664,24 +678,31 @@ async def check_cookies_health():
|
||||||
logger.warning(f"Проверка cookies {service} не удалась (сервис недоступен?): {e}")
|
logger.warning(f"Проверка cookies {service} не удалась (сервис недоступен?): {e}")
|
||||||
continue # не меняем состояние при сетевой ошибке — только при чистом ответе cookies_valid:false
|
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 # куки для этого сервиса не настроены — нечего проверять
|
continue # куки для этого сервиса не настроены — нечего проверять
|
||||||
|
|
||||||
currently_healthy = bool(data.get('cookies_valid'))
|
detail = data.get('detail', '')
|
||||||
|
currently_healthy = status == 'ok'
|
||||||
was_healthy = state.get(service, 'healthy') == 'healthy'
|
was_healthy = state.get(service, 'healthy') == 'healthy'
|
||||||
|
|
||||||
if not currently_healthy and was_healthy:
|
if not currently_healthy and was_healthy:
|
||||||
state[service] = 'broken'
|
state[service] = 'broken'
|
||||||
state.setdefault('last_reminder_ts', {})[service] = time.time()
|
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:
|
elif not currently_healthy and not was_healthy:
|
||||||
last = state.get('last_reminder_ts', {}).get(service, 0)
|
last = state.get('last_reminder_ts', {}).get(service, 0)
|
||||||
if time.time() - last >= COOKIE_REMINDER_INTERVAL:
|
if time.time() - last >= COOKIE_REMINDER_INTERVAL:
|
||||||
state.setdefault('last_reminder_ts', {})[service] = time.time()
|
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:
|
elif currently_healthy and not was_healthy:
|
||||||
state[service] = 'healthy'
|
state[service] = 'healthy'
|
||||||
await notify_admin_cookie_alert(service, healthy=True, detail='')
|
await notify_admin_cookie_alert(service, 'ok', '')
|
||||||
|
|
||||||
_save_cookie_state(state)
|
_save_cookie_state(state)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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` читает файл с диска при каждом запросе.
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
74
cookies/README.md
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
# cookies
|
||||||
|
|
||||||
|
Как обновлять `youtube_cookies.txt`/`instagram_cookies.txt` для прода и тестового стенда.
|
||||||
|
|
||||||
|
Куки обновляются **вручную и редко** — по алерту от проактивной проверки, а не по расписанию. Раньше здесь жил крон, который каждые 20–30 минут экспортировал куки из постоянно запущенного браузера; эта схема убрана, ниже объяснено почему.
|
||||||
|
|
||||||
|
## Почему больше нет крона и постоянно открытого браузера
|
||||||
|
|
||||||
|
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` на момент разбора имели срок ≈344–376 дней. Крон сотни раз в сутки копировал одни и те же значения.
|
||||||
|
|
||||||
|
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 восстановлены`.
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
#!/bin/bash
|
#!/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
|
set -euo pipefail
|
||||||
|
|
||||||
|
|
@ -9,8 +13,6 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
cd "$SCRIPT_DIR"
|
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"
|
INSTAGRAM_COOKIES="$SCRIPT_DIR/instagram_cookies.txt"
|
||||||
YOUTUBE_COOKIES="$SCRIPT_DIR/youtube_cookies.txt"
|
YOUTUBE_COOKIES="$SCRIPT_DIR/youtube_cookies.txt"
|
||||||
REMOTE_HOST="germany"
|
REMOTE_HOST="germany"
|
||||||
|
|
@ -19,7 +21,7 @@ REMOTE_YOUTUBE_PATH="/root/services/videoDownloadTGbot/youtube_cookies.txt"
|
||||||
DEV_HOST="hermesDev"
|
DEV_HOST="hermesDev"
|
||||||
DEV_INSTAGRAM_PATH="services/videoDownloadTGbot/instagram-downloader/instagram_cookies.txt"
|
DEV_INSTAGRAM_PATH="services/videoDownloadTGbot/instagram-downloader/instagram_cookies.txt"
|
||||||
DEV_YOUTUBE_PATH="services/videoDownloadTGbot/youtube-downloader/youtube_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() {
|
log() {
|
||||||
|
|
@ -32,48 +34,28 @@ error_exit() {
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
log "=== Начало обновления cookies ==="
|
# Проверяет, что файл существует и содержит хотя бы одну строку с cookie
|
||||||
|
check_cookies_file() {
|
||||||
|
local file="$1"
|
||||||
|
local name="$2"
|
||||||
|
|
||||||
# Проверяем наличие скриптов
|
if [ ! -f "$file" ]; then
|
||||||
if [ ! -f "$INSTAGRAM_SCRIPT" ]; then
|
error_exit "Файл не найден: $file. Сначала экспортируйте cookies $name — см. README.md"
|
||||||
error_exit "Скрипт не найден: $INSTAGRAM_SCRIPT"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ ! -f "$YOUTUBE_SCRIPT" ]; then
|
local lines
|
||||||
error_exit "Скрипт не найден: $YOUTUBE_SCRIPT"
|
lines=$(grep -cv -e '^#' -e '^$' "$file" || true)
|
||||||
|
if [ "$lines" -eq 0 ]; then
|
||||||
|
error_exit "Файл $file пустой (0 строк с cookies). Экспорт $name не удался — см. README.md"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Проверяем, что скрипты исполняемые
|
log "✅ $name: $file ($lines строк с cookies)"
|
||||||
chmod +x "$INSTAGRAM_SCRIPT" "$YOUTUBE_SCRIPT"
|
}
|
||||||
|
|
||||||
# Получаем cookies Instagram
|
log "=== Начало доставки cookies ==="
|
||||||
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
|
|
||||||
|
|
||||||
# Получаем cookies YouTube
|
check_cookies_file "$INSTAGRAM_COOKIES" "Instagram"
|
||||||
log "Получение cookies YouTube..."
|
check_cookies_file "$YOUTUBE_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
|
|
||||||
|
|
||||||
# Копируем cookies на удаленный хост (прод)
|
# Копируем cookies на удаленный хост (прод)
|
||||||
log "Копирование cookies на удаленный хост $REMOTE_HOST..."
|
log "Копирование cookies на удаленный хост $REMOTE_HOST..."
|
||||||
|
|
@ -107,5 +89,6 @@ else
|
||||||
log "⚠️ Не удалось скопировать YouTube cookies на $DEV_HOST (продолжаем)"
|
log "⚠️ Не удалось скопировать YouTube cookies на $DEV_HOST (продолжаем)"
|
||||||
fi
|
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
|
exit 0
|
||||||
|
|
@ -1,51 +1,7 @@
|
||||||
# Инструкция по получению cookies для Instagram
|
# Инструкция по получению cookies для Instagram
|
||||||
|
|
||||||
## Вариант 1: Использование расширения браузера (рекомендуется)
|
Актуальная инструкция живёт в одном месте — [`cookies/README.md`](../cookies/README.md).
|
||||||
|
|
||||||
1. Установите расширение для экспорта cookies:
|
Там описано: когда обновлять (по алерту `🍪⚠️ ПРОАКТИВНАЯ ПРОВЕРКА COOKIES` из админ-бота, а не по расписанию), как правильно экспортировать куки расширением для `cookies.txt` и как доставить их на прод через `deliver_cookies.sh`.
|
||||||
- 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
|
|
||||||
```
|
|
||||||
|
|
||||||
|
**Не используйте `yt-dlp --cookies-from-browser ... --cookies file.txt`** — этот способ раньше был описан здесь, но wiki yt-dlp его прямо запрещает: он ломает сессию, и куки протухают («cookies have likely been rotated in the browser as a security measure»).
|
||||||
|
|
|
||||||
|
|
@ -11,27 +11,9 @@
|
||||||
|
|
||||||
### 1. Настройка cookies
|
### 1. Настройка cookies
|
||||||
|
|
||||||
Перед запуском сервиса необходимо получить cookies Instagram. Есть несколько способов:
|
Перед запуском сервиса нужно положить `instagram_cookies.txt` в папку `instagram-downloader/`.
|
||||||
|
|
||||||
#### Способ 1: Через скрипт (рекомендуется)
|
Куки экспортируются вручную из браузера расширением для `cookies.txt` — пошаговая процедура в [`cookies/README.md`](../cookies/README.md). Скрипты автоматического экспорта через `yt-dlp --cookies-from-browser` убраны: этот способ ломает сессию, куки после него быстро протухают.
|
||||||
|
|
||||||
```bash
|
|
||||||
cd instagram-downloader
|
|
||||||
./get_instagram_cookies.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
Скрипт попросит выбрать браузер и автоматически извлечет cookies.
|
|
||||||
|
|
||||||
#### Способ 2: Обновление существующих cookies
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd instagram-downloader
|
|
||||||
./update_instagram_cookies.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Способ 3: Вручную
|
|
||||||
|
|
||||||
См. подробные инструкции в `INSTAGRAM_COOKIES_INSTRUCTIONS.md`
|
|
||||||
|
|
||||||
### 2. Запуск сервиса
|
### 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
|
### POST /download/stream
|
||||||
|
|
||||||
Скачивание видео с Instagram.
|
Скачивание видео с Instagram.
|
||||||
|
|
@ -93,40 +90,22 @@ curl http://localhost:5556/health
|
||||||
|
|
||||||
## Обновление cookies
|
## Обновление cookies
|
||||||
|
|
||||||
Cookies Instagram имеют ограниченный срок действия. Рекомендуется обновлять их раз в несколько недель.
|
Ключевые куки Instagram живут около года, поэтому обновлять их по расписанию не нужно. Триггер на обновление — алерт `🍪⚠️ ПРОАКТИВНАЯ ПРОВЕРКА COOKIES` из админ-бота (за состоянием следит фоновая проверка в `bot.py`, которая раз в 30 минут дёргает `POST /cookies/check`).
|
||||||
|
|
||||||
Для обновления:
|
Процедура экспорта и доставки — в [`cookies/README.md`](../cookies/README.md). Перезапускать сервис после обновления не нужно: `yt-dlp` читает файл с диска при каждом запросе.
|
||||||
|
|
||||||
```bash
|
|
||||||
cd instagram-downloader
|
|
||||||
./update_instagram_cookies.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
После обновления перезапустите сервис:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose restart instagram-downloader
|
|
||||||
```
|
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
### Сервис не может скачать видео
|
### Сервис не может скачать видео
|
||||||
|
|
||||||
1. Проверьте наличие файла `instagram_cookies.txt` в папке `instagram-downloader/`
|
1. Проверьте состояние куков: `curl -s -X POST localhost:5556/cookies/check`
|
||||||
2. Проверьте срок действия cookies (они могут истечь)
|
2. Проверьте наличие файла `instagram_cookies.txt` в папке `instagram-downloader/` и что в нём есть `sessionid`
|
||||||
3. Обновите cookies через скрипт `update_instagram_cookies.sh`
|
3. Если `cookies_valid: false` — обновите куки по инструкции в [`cookies/README.md`](../cookies/README.md)
|
||||||
4. Проверьте логи: `docker compose logs instagram-downloader`
|
4. Проверьте логи: `docker compose logs instagram-downloader`
|
||||||
|
|
||||||
### Cookies истекли
|
### Cookies есть и не истекли по сроку, но всё равно `login required`
|
||||||
|
|
||||||
Если видите ошибку "Instagram cookies истекли", выполните:
|
Instagram умеет блокировать сессию на своей стороне — тогда в логах видно `Main webpage is locked behind the login page` при формально валидном `sessionid`. Лечится только повторным экспортом куков. Провоцирует такую блокировку активная автоматическая работа с аккаунтом (например, постоянно открытая вкладка Instagram, непрерывно ходящая в сеть), поэтому после экспорта браузер лучше закрывать.
|
||||||
|
|
||||||
```bash
|
|
||||||
cd instagram-downloader
|
|
||||||
./update_instagram_cookies.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
Затем перезапустите сервис.
|
|
||||||
|
|
||||||
## Структура файлов
|
## Структура файлов
|
||||||
|
|
||||||
|
|
@ -136,9 +115,7 @@ instagram-downloader/
|
||||||
├── Dockerfile # Образ Docker
|
├── Dockerfile # Образ Docker
|
||||||
├── docker-compose.yml # Конфигурация для отдельного запуска
|
├── docker-compose.yml # Конфигурация для отдельного запуска
|
||||||
├── requirements.txt # Python зависимости
|
├── requirements.txt # Python зависимости
|
||||||
├── get_instagram_cookies.sh # Скрипт для получения cookies
|
├── INSTAGRAM_COOKIES_INSTRUCTIONS.md # Отсылка к cookies/README.md
|
||||||
├── update_instagram_cookies.sh # Скрипт для обновления cookies
|
|
||||||
├── INSTAGRAM_COOKIES_INSTRUCTIONS.md # Подробные инструкции по cookies
|
|
||||||
└── README.md # Этот файл
|
└── README.md # Этот файл
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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'])
|
@app.route('/cookies/check', methods=['POST'])
|
||||||
def cookies_check():
|
def cookies_check():
|
||||||
"""Проверяет, рабочие ли сейчас Instagram cookies (для проактивного мониторинга)."""
|
"""Проверяет, рабочие ли сейчас Instagram cookies (для проактивного мониторинга)."""
|
||||||
cookies_file = Path(os.getenv('INSTAGRAM_COOKIES_FILE', 'instagram_cookies.txt'))
|
cookies_file = Path(os.getenv('INSTAGRAM_COOKIES_FILE', 'instagram_cookies.txt'))
|
||||||
if not cookies_file.exists():
|
if not cookies_file.exists():
|
||||||
return jsonify({'cookies_present': False, 'cookies_valid': None,
|
return jsonify({'cookies_present': False, 'cookies_valid': None,
|
||||||
|
'status': 'no_cookies',
|
||||||
'detail': 'cookies file missing'}), 200
|
'detail': 'cookies file missing'}), 200
|
||||||
|
|
||||||
ydl_opts = {
|
ydl_opts = {
|
||||||
|
|
@ -224,10 +236,16 @@ def cookies_check():
|
||||||
try:
|
try:
|
||||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
||||||
ydl.extract_info(INSTAGRAM_COOKIE_TEST_URL, download=False)
|
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:
|
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,
|
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'])
|
@app.route('/download/stream', methods=['POST'])
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
Flask==3.0.0
|
Flask==3.0.0
|
||||||
flask-cors==4.0.0
|
flask-cors==4.0.0
|
||||||
yt-dlp>=2024.12.13
|
yt-dlp>=2026.7.4
|
||||||
|
|
||||||
gunicorn==21.2.0
|
gunicorn==21.2.0
|
||||||
|
|
|
||||||
|
|
@ -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
128
smoke_test.py
Executable 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())
|
||||||
|
|
@ -1,5 +1,20 @@
|
||||||
FROM python:3.11-slim
|
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
|
WORKDIR /app
|
||||||
COPY requirements.txt .
|
COPY requirements.txt .
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
|
||||||
|
|
@ -143,7 +143,7 @@ def _build_ytdlp_base_cmd() -> list:
|
||||||
YTDLP_CMD,
|
YTDLP_CMD,
|
||||||
'--socket-timeout', '15',
|
'--socket-timeout', '15',
|
||||||
'--extractor-args', EXTRACTOR_ARGS,
|
'--extractor-args', EXTRACTOR_ARGS,
|
||||||
'--js-runtimes', 'node',
|
'--js-runtimes', 'deno',
|
||||||
'--remote-components', 'ejs:github',
|
'--remote-components', 'ejs:github',
|
||||||
'--no-playlist',
|
'--no-playlist',
|
||||||
]
|
]
|
||||||
|
|
@ -490,7 +490,14 @@ YOUTUBE_COOKIE_TEST_URL = os.getenv(
|
||||||
'YOUTUBE_COOKIE_TEST_URL',
|
'YOUTUBE_COOKIE_TEST_URL',
|
||||||
'https://www.youtube.com/watch?v=jNQXAC9IVRw' # "Me at the zoo" — первое видео на YouTube, публичное, без возрастных ограничений
|
'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'])
|
@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'))
|
cookies_file = Path(os.getenv('YOUTUBE_COOKIES_FILE', '/app/youtube_cookies.txt'))
|
||||||
if not (cookies_file.exists() and cookies_file.stat().st_size > 0):
|
if not (cookies_file.exists() and cookies_file.stat().st_size > 0):
|
||||||
return jsonify({'cookies_present': False, 'cookies_valid': None,
|
return jsonify({'cookies_present': False, 'cookies_valid': None,
|
||||||
|
'status': 'no_cookies',
|
||||||
'detail': 'cookies file missing or empty'}), 200
|
'detail': 'cookies file missing or empty'}), 200
|
||||||
|
|
||||||
# Без --quiet/--no-warnings: сигнал ротации кук — это подавляемый WARNING, а не ошибка с ненулевым returncode
|
# Без --quiet/--no-warnings: сигнал ротации кук — это подавляемый WARNING, а не ошибка с ненулевым returncode
|
||||||
|
|
@ -506,15 +514,23 @@ def cookies_check():
|
||||||
try:
|
try:
|
||||||
result = _run_ytdlp(cmd, timeout=INFO_TIMEOUT)
|
result = _run_ytdlp(cmd, timeout=INFO_TIMEOUT)
|
||||||
except Exception as e:
|
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
|
'detail': f'yt-dlp invocation failed: {e}'}), 200
|
||||||
|
|
||||||
stderr = result.stderr
|
stderr = result.stderr
|
||||||
if any(marker in stderr for marker in YOUTUBE_COOKIE_INVALID_MARKERS) or result.returncode != 0:
|
detail = stderr.strip()[-500:]
|
||||||
return jsonify({'cookies_present': True, 'cookies_valid': False,
|
|
||||||
'detail': stderr.strip()[-500:]}), 200
|
|
||||||
|
|
||||||
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'])
|
@app.route('/download/stream', methods=['POST'])
|
||||||
|
|
@ -570,9 +586,10 @@ def download_stream():
|
||||||
error_msg = (
|
error_msg = (
|
||||||
f"{error_str}\n\n"
|
f"{error_str}\n\n"
|
||||||
"💡 Совет: Cookies устарели или недействительны. "
|
"💡 Совет: Cookies устарели или недействительны. "
|
||||||
"Обновите cookies через скрипт:\n"
|
"Обновите их вручную по инструкции в cookies/README.md "
|
||||||
" ./youtube-downloader/get_youtube_cookies.sh\n"
|
"(для YouTube — только через приватное окно, иначе сессия ротируется) "
|
||||||
"Затем перезапустите сервис."
|
"и доставьте через cookies/deliver_cookies.sh. "
|
||||||
|
"Перезапуск сервиса не требуется."
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
error_msg = error_str
|
error_msg = error_str
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
Flask==3.0.0
|
Flask==3.0.0
|
||||||
flask-cors==4.0.0
|
flask-cors==4.0.0
|
||||||
yt-dlp>=2025.12.01
|
yt-dlp>=2026.7.4
|
||||||
gunicorn==21.2.0
|
gunicorn==21.2.0
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue