fix(cookies): не давать yt-dlp затирать мастер-файл кук, честнее алерты
All checks were successful
CI/CD Pipeline / build-and-deploy (push) Successful in 52s
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:
parent
8fa53f7cfc
commit
c9972f3d68
3 changed files with 93 additions and 25 deletions
|
|
@ -6,6 +6,7 @@ YouTube Video Downloader Service
|
|||
"""
|
||||
import os
|
||||
import time
|
||||
import shutil
|
||||
import logging
|
||||
import traceback
|
||||
import subprocess
|
||||
|
|
@ -135,6 +136,13 @@ INFO_TIMEOUT = 60
|
|||
|
||||
EXTRACTOR_ARGS = 'youtube:player_client=web,android;skip=translated_subs,hls'
|
||||
|
||||
# yt-dlp трактует --cookies как read-write: после каждого запуска он дописывает cookie
|
||||
# jar обратно в файл (--help: "read cookies from and dump cookie jar in"), а когда
|
||||
# решает, что cookies "no longer valid" — сам чистит куки сессии из jar'а и тут же
|
||||
# сохраняет это на диск. Поэтому yt-dlp всегда получает одноразовую копию, мастер-файл
|
||||
# он больше не видит. Фиксированное имя безопасно — сервис в один воркер (--workers=1).
|
||||
YOUTUBE_COOKIES_SCRATCH = Path('/tmp/youtube_cookies_scratch.txt')
|
||||
|
||||
|
||||
def _build_ytdlp_base_cmd() -> list:
|
||||
"""Базовые аргументы yt-dlp CLI."""
|
||||
|
|
@ -148,7 +156,8 @@ def _build_ytdlp_base_cmd() -> list:
|
|||
'--no-playlist',
|
||||
]
|
||||
if cookies_file.exists() and cookies_file.stat().st_size > 0:
|
||||
cmd += ['--cookies', str(cookies_file.absolute())]
|
||||
shutil.copy2(cookies_file, YOUTUBE_COOKIES_SCRATCH)
|
||||
cmd += ['--cookies', str(YOUTUBE_COOKIES_SCRATCH)]
|
||||
return cmd
|
||||
|
||||
|
||||
|
|
@ -506,7 +515,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',
|
||||
'status': 'no_cookies', 'test_url': YOUTUBE_COOKIE_TEST_URL,
|
||||
'detail': 'cookies file missing or empty'}), 200
|
||||
|
||||
# Без --quiet/--no-warnings: сигнал ротации кук — это подавляемый WARNING, а не ошибка с ненулевым returncode
|
||||
|
|
@ -515,7 +524,7 @@ def cookies_check():
|
|||
result = _run_ytdlp(cmd, timeout=INFO_TIMEOUT)
|
||||
except Exception as e:
|
||||
return jsonify({'cookies_present': True, 'cookies_valid': None,
|
||||
'status': 'extraction_failed',
|
||||
'status': 'extraction_failed', 'test_url': YOUTUBE_COOKIE_TEST_URL,
|
||||
'detail': f'yt-dlp invocation failed: {e}'}), 200
|
||||
|
||||
stderr = result.stderr
|
||||
|
|
@ -523,14 +532,16 @@ def cookies_check():
|
|||
|
||||
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
|
||||
'status': 'cookies_invalid', 'test_url': YOUTUBE_COOKIE_TEST_URL,
|
||||
'detail': detail}), 200
|
||||
|
||||
if result.returncode != 0:
|
||||
return jsonify({'cookies_present': True, 'cookies_valid': None,
|
||||
'status': 'extraction_failed', 'detail': detail}), 200
|
||||
'status': 'extraction_failed', 'test_url': YOUTUBE_COOKIE_TEST_URL,
|
||||
'detail': detail}), 200
|
||||
|
||||
return jsonify({'cookies_present': True, 'cookies_valid': True,
|
||||
'status': 'ok', 'detail': ''}), 200
|
||||
'status': 'ok', 'test_url': YOUTUBE_COOKIE_TEST_URL, 'detail': ''}), 200
|
||||
|
||||
|
||||
@app.route('/download/stream', methods=['POST'])
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue