fix(instagram): смягчить /cookies/check, честное сообщение для гейтованного контента
All checks were successful
CI/CD Pipeline / build-and-deploy (push) Successful in 1m0s
All checks were successful
CI/CD Pipeline / build-and-deploy (push) Successful in 1m0s
/cookies/check раньше тестировал только путь с cookies и при неудаче писал
cookies_invalid — даже когда реальное скачивание прекрасно работает через
app-level fallback без cookies. Теперь проверка повторяет этот же
эффективный путь: cookies path сломан известным багом yt-dlp (media/info
всегда 404 вместо редиректа на логин), но раз fallback его закрывает —
это не авария и не повод слать ложный алерт "куки протухли", который
обновление cookies всё равно не лечит. Если не работает и анонимный путь
(который cookies не использует) — это extraction_failed, а не cookies_invalid.
Отдельно: когда 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
b5f0670754
commit
8fa53f7cfc
2 changed files with 92 additions and 75 deletions
12
bot.py
12
bot.py
|
|
@ -45,6 +45,11 @@ VK_DOWNLOADER_URL = os.getenv('VK_DOWNLOADER_URL', 'http://localhost:5555')
|
|||
YAPFILES_DOWNLOADER_URL = os.getenv('YAPFILES_DOWNLOADER_URL', 'http://localhost:5558')
|
||||
TIKTOK_DOWNLOADER_URL = os.getenv('TIKTOK_DOWNLOADER_URL', 'http://localhost:5559')
|
||||
|
||||
# Instagram сам отказывает в показе такого контента анонимным/несовместимым аккаунтам —
|
||||
# повторная попытка не поможет, поэтому пользователю нужен отдельный, понятный текст
|
||||
# вместо общего "Something went wrong"
|
||||
INSTAGRAM_SENSITIVE_CONTENT_MARKERS = ("can't be seen by certain audiences", "isn't available to everyone")
|
||||
|
||||
# Базовая директория проекта (абсолютный путь), чтобы не зависеть от рабочей директории процесса
|
||||
BASE_DIR = Path(__file__).resolve().parent
|
||||
|
||||
|
|
@ -111,6 +116,7 @@ TEXTS = {
|
|||
'sending': "📤 Отправляю видео...",
|
||||
'caption': "Видео скачано с @{bot_username}",
|
||||
'error': "❌ Something went wrong while processing your video. Please try again in a bit.",
|
||||
'error_sensitive_content': "⚠️ This content is restricted by Instagram (age/sensitive content) and can't be downloaded by the bot. Try opening the link directly in the Instagram app.",
|
||||
'error_unknown_source': "Пардон, не умеем работать с этим источником",
|
||||
'error_vk_not_video': "❌ Это ссылка VK, но не на видео. Пришлите ссылку вида vk.com/video... или vk.com/clip...",
|
||||
'error_file_too_large': "❌ Видео слишком большое ({size_mb:.1f} МБ, max = 50)",
|
||||
|
|
@ -169,6 +175,7 @@ TEXTS = {
|
|||
'sending': "📤 Sending video...",
|
||||
'caption': "Video downloaded via @{bot_username}",
|
||||
'error': "❌ Something went wrong while processing your video. Please try again in a bit.",
|
||||
'error_sensitive_content': "⚠️ This content is restricted by Instagram (age/sensitive content) and can't be downloaded by the bot. Try opening the link directly in the Instagram app.",
|
||||
'error_unknown_source': "Sorry, this source is not supported",
|
||||
'error_vk_not_video': "❌ This is a VK link, but not a video. Send a vk.com/video... or vk.com/clip... link.",
|
||||
'error_file_too_large': "❌ Video is too large ({size_mb:.1f} MB, max = 50)",
|
||||
|
|
@ -841,7 +848,10 @@ async def process_queue_item(item: QueueItem):
|
|||
|
||||
await notify_admin_error(item.url, str(e), item.original_message.from_user)
|
||||
|
||||
error_msg = get_text(item.locale, 'error')
|
||||
if any(marker in str(e).lower() for marker in INSTAGRAM_SENSITIVE_CONTENT_MARKERS):
|
||||
error_msg = get_text(item.locale, 'error_sensitive_content')
|
||||
else:
|
||||
error_msg = get_text(item.locale, 'error')
|
||||
try:
|
||||
await item.status_message.edit_text(error_msg)
|
||||
except:
|
||||
|
|
|
|||
|
|
@ -141,56 +141,54 @@ def download_instagram_video(url: str, max_retries: int = 3) -> Path:
|
|||
except Exception as e:
|
||||
logger.warning(f"Не удалось прочитать cookies: {e}")
|
||||
|
||||
def _build_opts(use_cookies: bool) -> dict:
|
||||
ydl_opts = {
|
||||
'format': 'best',
|
||||
'outtmpl': str(DOWNLOADS_DIR / f'{uuid.uuid4()}_%(title)s.%(ext)s'),
|
||||
'quiet': False,
|
||||
'no_warnings': False,
|
||||
'socket_timeout': 30,
|
||||
}
|
||||
if use_cookies:
|
||||
ydl_opts['cookiefile'] = str(cookies_file_path.absolute())
|
||||
headers = {
|
||||
'Referer': 'https://www.instagram.com/',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
}
|
||||
if csrf_token:
|
||||
headers['X-CSRFToken'] = csrf_token
|
||||
ydl_opts['http_headers'] = headers
|
||||
return ydl_opts
|
||||
|
||||
# Протухшая сессия заставляет yt-dlp ходить в API, который отдаёт 404, тогда как
|
||||
# анонимный путь через веб-страницу для публичных постов работает. Поэтому после
|
||||
# неудачи с cookies пробуем без них — как это давно делает YouTube-загрузчик.
|
||||
cookie_modes = [True, False] if cookies_file_path.exists() else [False]
|
||||
|
||||
last_error = None
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
# Базовые настройки
|
||||
ydl_opts = {
|
||||
'format': 'best',
|
||||
'outtmpl': str(DOWNLOADS_DIR / f'{uuid.uuid4()}_%(title)s.%(ext)s'),
|
||||
'quiet': False,
|
||||
'no_warnings': False,
|
||||
'socket_timeout': 30,
|
||||
}
|
||||
|
||||
# Если есть файл с cookies, используем его
|
||||
if cookies_file_path.exists():
|
||||
# Используем абсолютный путь к cookies
|
||||
ydl_opts['cookiefile'] = str(cookies_file_path.absolute())
|
||||
logger.info(f"Instagram: используем cookies из {cookies_file_path}")
|
||||
|
||||
# Добавляем заголовки с csrf token если есть
|
||||
headers = {
|
||||
'Referer': 'https://www.instagram.com/',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
}
|
||||
if csrf_token:
|
||||
headers['X-CSRFToken'] = csrf_token
|
||||
logger.info(f"Instagram: добавлен csrf token в заголовки")
|
||||
if sessionid:
|
||||
logger.info(f"Instagram: sessionid найден (длина: {len(sessionid)})")
|
||||
|
||||
ydl_opts['http_headers'] = headers
|
||||
|
||||
logger.info(f"Instagram: начинаем скачивание (попытка {attempt + 1}/{max_retries})")
|
||||
|
||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
||||
ydl.download([url])
|
||||
|
||||
# Находим скачанный файл
|
||||
downloaded_files = list(DOWNLOADS_DIR.glob('*'))
|
||||
if downloaded_files:
|
||||
downloaded_files.sort(key=lambda x: x.stat().st_mtime, reverse=True)
|
||||
return downloaded_files[0]
|
||||
else:
|
||||
for use_cookies in cookie_modes:
|
||||
mode = "с cookies" if use_cookies else "без cookies"
|
||||
try:
|
||||
logger.info(f"Instagram: скачивание {mode} (попытка {attempt + 1}/{max_retries})")
|
||||
|
||||
with yt_dlp.YoutubeDL(_build_opts(use_cookies)) as ydl:
|
||||
ydl.download([url])
|
||||
|
||||
# Находим скачанный файл
|
||||
downloaded_files = list(DOWNLOADS_DIR.glob('*'))
|
||||
if downloaded_files:
|
||||
downloaded_files.sort(key=lambda x: x.stat().st_mtime, reverse=True)
|
||||
return downloaded_files[0]
|
||||
raise Exception("Файл не был найден после скачивания")
|
||||
|
||||
except Exception as e:
|
||||
last_error = e
|
||||
logger.warning(f"Instagram: попытка {attempt + 1}/{max_retries} не удалась: {e}")
|
||||
if attempt < max_retries - 1:
|
||||
time.sleep((attempt + 1) * 2)
|
||||
|
||||
|
||||
except Exception as e:
|
||||
last_error = e
|
||||
logger.warning(f"Instagram: {mode}, попытка {attempt + 1}/{max_retries} не удалась: {e}")
|
||||
|
||||
if attempt < max_retries - 1:
|
||||
time.sleep((attempt + 1) * 2)
|
||||
|
||||
raise last_error or Exception("Неизвестная ошибка при скачивании с Instagram. Возможно, нужно обновить cookies.")
|
||||
|
||||
|
||||
|
|
@ -206,46 +204,55 @@ 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',
|
||||
)
|
||||
def _probe_extract(url: str, cookies_file: Path | None) -> tuple[bool, str]:
|
||||
"""Пробует достать метаданные (без скачивания). Возвращает (получилось, текст ошибки)."""
|
||||
ydl_opts = {'quiet': True, 'no_warnings': True, 'socket_timeout': 20}
|
||||
if cookies_file is not None:
|
||||
ydl_opts['cookiefile'] = str(cookies_file.absolute())
|
||||
ydl_opts['http_headers'] = {'Referer': 'https://www.instagram.com/'}
|
||||
try:
|
||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
||||
ydl.extract_info(url, download=False)
|
||||
return True, ''
|
||||
except Exception as e:
|
||||
return False, str(e)
|
||||
|
||||
|
||||
@app.route('/cookies/check', methods=['POST'])
|
||||
def cookies_check():
|
||||
"""Проверяет, рабочие ли сейчас Instagram cookies (для проактивного мониторинга)."""
|
||||
"""Проверяет, способен ли сервис прямо сейчас скачать публичное видео.
|
||||
|
||||
Намеренно повторяет эффективный путь download_instagram_video: сначала с cookies,
|
||||
при неудаче — без них. Известный баг yt-dlp (эндпоинт media/{id}/info/ отдаёт 404
|
||||
вместо редиректа на логин даже с валидной sessionid, из-за чего внутренний фоллбэк
|
||||
yt-dlp не срабатывает) означает, что путь с cookies может не проходить всегда —
|
||||
но раз наш собственный фоллбэк без cookies его закрывает, для пользователя это не
|
||||
авария и не повод слать алерт "куки протухли" (обновление 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 = {
|
||||
'quiet': True,
|
||||
'no_warnings': True,
|
||||
'socket_timeout': 20,
|
||||
'cookiefile': str(cookies_file.absolute()),
|
||||
'http_headers': {'Referer': 'https://www.instagram.com/'},
|
||||
}
|
||||
try:
|
||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
||||
ydl.extract_info(INSTAGRAM_COOKIE_TEST_URL, download=False)
|
||||
ok_with, err_with = _probe_extract(INSTAGRAM_COOKIE_TEST_URL, cookies_file)
|
||||
if ok_with:
|
||||
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,
|
||||
'status': 'cookies_invalid', 'detail': detail}), 200
|
||||
|
||||
ok_without, err_without = _probe_extract(INSTAGRAM_COOKIE_TEST_URL, None)
|
||||
if ok_without:
|
||||
# Путь с cookies сломан, но эффективное скачивание всё равно работает через
|
||||
# фоллбэк — так же, как реальный download_instagram_video. Не поднимаем тревогу.
|
||||
detail = f"cookies-путь сломан, но fallback без cookies работает: {err_with[-400:]}"
|
||||
return jsonify({'cookies_present': True, 'cookies_valid': None,
|
||||
'status': 'extraction_failed', 'detail': detail}), 200
|
||||
'status': 'ok', 'detail': detail}), 200
|
||||
|
||||
# Не работает и анонимный путь, который cookies вообще не использует —
|
||||
# значит дело не в cookies, а в более общей поломке (сайт/экстрактор/сеть).
|
||||
return jsonify({'cookies_present': True, 'cookies_valid': None,
|
||||
'status': 'extraction_failed', 'detail': err_without[-500:]}), 200
|
||||
|
||||
|
||||
@app.route('/download/stream', methods=['POST'])
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue