feat(admin_bot): показывать состояние загрузчиков в /stat
All checks were successful
CI/CD Pipeline / build-and-deploy (push) Successful in 47s

Раньше о поломке сервиса админ узнавал только из алерта health-check или
из ошибки живого пользователя. Теперь /stat отдаёт состояние всех пяти
загрузчиков по запросу.

У YouTube и Instagram дёргается /cookies/check, поэтому статусы различают
причину: работает / cookies протухли / не извлекает видео (не cookies) /
недоступен. Остальные три проверяются через /health.

Проверка ходит в реальные YouTube и Instagram и занимает секунды, поэтому
статистика отправляется сразу, а блок состояния дозаполняется правкой
сообщения. Все сервисы опрашиваются параллельно — последовательно набежало
бы под полминуты.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
vrubelroman 2026-07-26 00:40:32 +00:00
parent d597a5e1c5
commit b5f0670754

View file

@ -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):