From b5f0670754211b83af380a8b09b37535eeb6a0d7 Mon Sep 17 00:00:00 2001 From: vrubelroman Date: Sun, 26 Jul 2026 00:40:32 +0000 Subject: [PATCH] =?UTF-8?q?feat(admin=5Fbot):=20=D0=BF=D0=BE=D0=BA=D0=B0?= =?UTF-8?q?=D0=B7=D1=8B=D0=B2=D0=B0=D1=82=D1=8C=20=D1=81=D0=BE=D1=81=D1=82?= =?UTF-8?q?=D0=BE=D1=8F=D0=BD=D0=B8=D0=B5=20=D0=B7=D0=B0=D0=B3=D1=80=D1=83?= =?UTF-8?q?=D0=B7=D1=87=D0=B8=D0=BA=D0=BE=D0=B2=20=D0=B2=20/stat?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Раньше о поломке сервиса админ узнавал только из алерта health-check или из ошибки живого пользователя. Теперь /stat отдаёт состояние всех пяти загрузчиков по запросу. У YouTube и Instagram дёргается /cookies/check, поэтому статусы различают причину: работает / cookies протухли / не извлекает видео (не cookies) / недоступен. Остальные три проверяются через /health. Проверка ходит в реальные YouTube и Instagram и занимает секунды, поэтому статистика отправляется сразу, а блок состояния дозаполняется правкой сообщения. Все сервисы опрашиваются параллельно — последовательно набежало бы под полминуты. Co-Authored-By: Claude Opus 5 --- admin_bot.py | 70 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 69 insertions(+), 1 deletion(-) diff --git a/admin_bot.py b/admin_bot.py index 23340fc..e4e6b13 100644 --- a/admin_bot.py +++ b/admin_bot.py @@ -7,6 +7,8 @@ import asyncio import logging import sqlite3 from pathlib import Path + +import httpx from telegram import Bot, Update from telegram.ext import Application, CommandHandler, ContextTypes, MessageHandler, filters from telegram.request import HTTPXRequest @@ -31,6 +33,57 @@ DATA_DIR = BASE_DIR / 'data' DB_FILE = DATA_DIR / 'bot.db' ADMIN_CHAT_ID_FILE = DATA_DIR / 'admin_chat_id.txt' +# Адреса загрузчиков — те же дефолты, что и в bot.py (все сервисы в network_mode: host). +# Флаг has_cookies: у YouTube и Instagram есть /cookies/check, у остальных только /health. +DOWNLOADER_SERVICES = [ + ('YouTube', os.getenv('YOUTUBE_DOWNLOADER_URL', 'http://localhost:5557'), True), + ('Instagram', os.getenv('INSTAGRAM_DOWNLOADER_URL', 'http://localhost:5556'), True), + ('VK', os.getenv('VK_DOWNLOADER_URL', 'http://localhost:5555'), False), + ('Yapfiles', os.getenv('YAPFILES_DOWNLOADER_URL', 'http://localhost:5558'), False), + ('TikTok', os.getenv('TIKTOK_DOWNLOADER_URL', 'http://localhost:5559'), False), +] + +# /cookies/check ходит в реальный YouTube/Instagram, это несколько секунд +SERVICE_CHECK_TIMEOUT = httpx.Timeout(connect=5, read=45, write=10, pool=10) + +# Расшифровка status из /cookies/check +COOKIE_STATUS_LABELS = { + 'ok': '✅ работает', + 'cookies_invalid': '🍪 cookies протухли', + 'extraction_failed': '⚠️ не извлекает видео (не cookies)', + 'no_cookies': '➖ без cookies', +} + + +async def check_service_status(name: str, base_url: str, has_cookies: bool) -> str: + """Возвращает строку статуса одного загрузчика для /stat.""" + try: + async with httpx.AsyncClient(timeout=SERVICE_CHECK_TIMEOUT) as client: + if has_cookies: + response = await client.post(f'{base_url}/cookies/check') + if response.status_code != 200: + return f"• {name}: ❌ HTTP {response.status_code}" + status = response.json().get('status', 'unknown') + label = COOKIE_STATUS_LABELS.get(status, f'⚠️ {status}') + return f"• {name}: {label}" + + response = await client.get(f'{base_url}/health') + if response.status_code != 200: + return f"• {name}: ❌ HTTP {response.status_code}" + return f"• {name}: ✅ работает" + except Exception as e: + logger.warning(f"Проверка сервиса {name} не удалась: {e}") + return f"• {name}: ❌ недоступен" + + +async def get_services_status_text() -> str: + """Опрашивает все загрузчики параллельно — последовательно это заняло бы десятки секунд.""" + results = await asyncio.gather(*( + check_service_status(name, url, has_cookies) + for name, url, has_cookies in DOWNLOADER_SERVICES + )) + return "\n".join(results) + def get_total_downloads() -> int: """Возвращает общее количество скачанных видео""" @@ -256,7 +309,22 @@ async def stat_command(update: Update, context: ContextTypes.DEFAULT_TYPE): f"❌ Ошибки по сервисам:\n{error_stats_text}" ) - await update.message.reply_text(stat_message) + # Проверка сервисов ходит в реальные YouTube/Instagram и занимает секунды, + # поэтому сначала отдаём статистику, а статус дозаполняем правкой сообщения + sent = await update.message.reply_text( + f"{stat_message}\n\n🔌 Состояние сервисов:\nпроверяю…" + ) + + try: + services_text = await get_services_status_text() + except Exception as e: + logger.error(f"Не удалось получить состояние сервисов: {e}") + services_text = "❌ не удалось проверить" + + try: + await sent.edit_text(f"{stat_message}\n\n🔌 Состояние сервисов:\n{services_text}") + except Exception as e: + logger.warning(f"Не удалось обновить сообщение статистики: {e}") async def send_messages_command(update: Update, context: ContextTypes.DEFAULT_TYPE):