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 logging
import sqlite3 import sqlite3
from pathlib import Path from pathlib import Path
import httpx
from telegram import Bot, Update from telegram import Bot, Update
from telegram.ext import Application, CommandHandler, ContextTypes, MessageHandler, filters from telegram.ext import Application, CommandHandler, ContextTypes, MessageHandler, filters
from telegram.request import HTTPXRequest from telegram.request import HTTPXRequest
@ -31,6 +33,57 @@ DATA_DIR = BASE_DIR / 'data'
DB_FILE = DATA_DIR / 'bot.db' DB_FILE = DATA_DIR / 'bot.db'
ADMIN_CHAT_ID_FILE = DATA_DIR / 'admin_chat_id.txt' 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: def get_total_downloads() -> int:
"""Возвращает общее количество скачанных видео""" """Возвращает общее количество скачанных видео"""
@ -256,7 +309,22 @@ async def stat_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
f"❌ Ошибки по сервисам:\n{error_stats_text}" 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): async def send_messages_command(update: Update, context: ContextTypes.DEFAULT_TYPE):