fix(cookies): не давать yt-dlp затирать мастер-файл кук, честнее алерты
All checks were successful
CI/CD Pipeline / build-and-deploy (push) Successful in 52s

Реальный инцидент: cookies "протухли" за пару дней вместо ~года. Причина —
не срок годности, а сам yt-dlp: --cookies FILE читает И дописывает cookie
jar обратно в файл после каждого запуска (--help: "read cookies from and
dump cookie jar in"), а когда Instagram-экстрактор решает, что сессия
невалидна, он явно чистит sessionid из jar'а — и это тут же сохраняется на
диск через YoutubeDL.close(). Наш собственный health-check (каждые 30 мин)
и обычные скачивания медленно, но верно стирали себе рабочие cookies.

Фикс: yt-dlp больше никогда не видит мастер-файл, только одноразовую копию
в фиксированном /tmp-пути (безопасно — оба сервиса --workers=1, гонок нет).
Проверено: md5sum/mtime мастер-файлов не меняются ни после серии
/cookies/check, ни после реального /download/stream.

Заодно в bot.py: notify_admin_cookie_alert больше не заявляет "это НЕ
cookies" для extraction_failed — на практике это оказалось не всегда
верно (анонимный rate-limit тоже "не cookies" по факту, но валидная
сессия могла бы его обойти). В алерты добавлена проверяемая ссылка
(test_url из /cookies/check), чтобы сразу было видно, что это health-check
дёргает тестовый ролик, а не реальная ссылка пользователя. Новый статус
cookies_incomplete детектирует "файл есть, но sessionid нет" ещё до
сетевых проверок — ловит именно тот случай, что привёл к инциденту.

Отдельно: пользователю теперь показывается понятное сообщение, когда
Instagram сам блокирует контент как возрастной/чувствительный
("can't be seen by certain audiences") — вместо общего "Something went
wrong", раз повторная попытка всё равно не поможет. Админ по-прежнему
получает полный технический текст без изменений.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
vrubelroman 2026-07-31 15:18:13 +00:00
parent 8fa53f7cfc
commit c9972f3d68
3 changed files with 93 additions and 25 deletions

34
bot.py
View file

@ -624,12 +624,14 @@ def _save_cookie_state(state: dict):
logger.error(f"Не удалось сохранить состояние cookie-health: {e}")
async def notify_admin_cookie_alert(service: str, status: str, detail: str):
async def notify_admin_cookie_alert(service: str, status: str, detail: str, test_url: str = ''):
"""Проактивное уведомление от health-check, не связано с ошибкой конкретного пользователя.
status: 'ok' | 'cookies_invalid' | 'extraction_failed'
Тексты разные намеренно: сбой извлечения (версия yt-dlp, JS-рантайм) не должен
выглядеть как протухшие cookies, иначе разбор уходит не туда.
status: 'ok' | 'cookies_invalid' | 'cookies_incomplete' | 'extraction_failed'
Тексты разные намеренно, но НЕ утверждают того, чего не знаем: extraction_failed
это просто "не получилось", а не гарантия, что дело не в cookies (на практике оба
пути с cookies и анонимный иногда падают по причинам, так или иначе связанным
с cookies, например анонимный rate-limit, который валидная сессия могла бы обойти).
"""
if not ADMIN_BOT_TOKEN:
return
@ -646,18 +648,27 @@ async def notify_admin_cookie_alert(service: str, status: str, detail: str):
)
admin_bot = Bot(token=ADMIN_BOT_TOKEN, request=request)
url_line = f"🔗 Проверялось на: {test_url}\n" if test_url else ""
if status == 'ok':
text = f"✅ Снова работает\n\n🍪 Сервис: {service}\nПроверка проходит успешно."
text = f"✅ Снова работает\n\n🍪 Сервис: {service}\n{url_line}Проверка проходит успешно."
elif status == 'cookies_invalid':
text = (
f"🍪⚠️ COOKIES ПРОТУХЛИ\n\n🍪 Сервис: {service}\n{detail}\n\n"
f"🍪⚠️ COOKIES ПРОТУХЛИ\n\n🍪 Сервис: {service}\n{url_line}{detail}\n\n"
"Обновить по инструкции в cookies/README.md "
"(для YouTube — только через приватное окно)."
)
elif status == 'cookies_incomplete':
text = (
f"🍪⚠️ COOKIES НЕПОЛНЫЕ\n\n🍪 Сервис: {service}\n{url_line}{detail}\n\n"
"Файл cookies есть, но реальной сессии в нём нет (похоже на неправильный "
"экспорт). Переэкспортировать по инструкции в cookies/README.md."
)
else:
text = (
f"🛠⚠️ СЕРВИС НЕ МОЖЕТ ИЗВЛЕЧЬ ВИДЕО\n\n🍪 Сервис: {service}\n{detail}\n\n"
"Это НЕ cookies — проверить версию yt-dlp и логи сервиса."
f"🛠⚠️ СЕРВИС НЕ МОЖЕТ ИЗВЛЕЧЬ ВИДЕО\n\n🍪 Сервис: {service}\n{url_line}{detail}\n\n"
"Причина неясна — может быть версия yt-dlp/JS-рантайм, может быть и cookies. "
"Смотреть логи сервиса и текст ошибки выше."
)
if len(text) > 4000:
text = text[:4000] + ""
@ -695,21 +706,22 @@ async def check_cookies_health():
continue # куки для этого сервиса не настроены — нечего проверять
detail = data.get('detail', '')
test_url = data.get('test_url', '')
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, status, detail)
await notify_admin_cookie_alert(service, status, detail, test_url)
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, status, detail)
await notify_admin_cookie_alert(service, status, detail, test_url)
elif currently_healthy and not was_healthy:
state[service] = 'healthy'
await notify_admin_cookie_alert(service, 'ok', '')
await notify_admin_cookie_alert(service, 'ok', '', test_url)
_save_cookie_state(state)