diff --git a/main.py b/main.py index 49e91e5..5ba90df 100644 --- a/main.py +++ b/main.py @@ -5,8 +5,14 @@ from telegram.ext import ( MessageHandler, filters, ) +from telegram.request import HTTPXRequest -from src.config import BOT_TOKEN +from src.config import ( + BOT_TOKEN, + TELEGRAM_CONNECT_TIMEOUT, + TELEGRAM_READ_TIMEOUT, + TELEGRAM_WRITE_TIMEOUT, +) from src.database import init_db from src.handlers import ( admin_command, @@ -41,7 +47,19 @@ def main() -> None: init_db() - app = ApplicationBuilder().token(BOT_TOKEN).build() + app = ( + ApplicationBuilder() + .token(BOT_TOKEN) + .request( + HTTPXRequest( + connection_pool_size=8, + connect_timeout=TELEGRAM_CONNECT_TIMEOUT, + read_timeout=TELEGRAM_READ_TIMEOUT, + write_timeout=TELEGRAM_WRITE_TIMEOUT, + ) + ) + .build() + ) # Команды app.add_handler(CommandHandler("start", start_command)) diff --git a/src/audio.py b/src/audio.py index 1699618..06398d9 100644 --- a/src/audio.py +++ b/src/audio.py @@ -1,6 +1,9 @@ -from telegram import Bot +import asyncio -from src.config import DATA_PARTIAL_DIR +from telegram import Bot +from telegram.error import NetworkError, TimedOut + +from src.config import DATA_PARTIAL_DIR, MAX_RETRY_ATTEMPTS, RETRY_DELAYS from src.database import upsert_recording from src.logger import logger from src.scenarios import get_audio_filename @@ -15,22 +18,38 @@ async def save_voice_message( replica_index: int, duration: int, ) -> None: - """Сохраняет голосовое сообщение в data_partial/.""" - # Создаём директорию если нужно + """Сохраняет голосовое сообщение в data_partial/ с retry логикой.""" scenario_dir = DATA_PARTIAL_DIR / scenario_id scenario_dir.mkdir(parents=True, exist_ok=True) - # Скачиваем файл - file = await bot.get_file(file_id) filename = get_audio_filename(replica_index, speaker_id, user_id) filepath = scenario_dir / filename - await file.download_to_drive(filepath) + for attempt in range(MAX_RETRY_ATTEMPTS): + try: + file = await bot.get_file(file_id) + await file.download_to_drive(filepath) - # Записываем в БД (duration из метаданных Telegram) - upsert_recording(user_id, scenario_id, replica_index, float(duration)) + # Успех - записываем в БД + upsert_recording(user_id, scenario_id, replica_index, float(duration)) + logger.debug(f"Saved voice: {filepath} ({duration}s)") + return - logger.debug(f"Saved voice: {filepath} ({duration}s)") + except (TimedOut, NetworkError) as e: + if attempt < MAX_RETRY_ATTEMPTS - 1: + delay = RETRY_DELAYS[attempt] + logger.warning( + f"Network error on attempt {attempt + 1}/" + f"{MAX_RETRY_ATTEMPTS}: {e}. " + f"Retrying in {delay}s..." + ) + await asyncio.sleep(delay) + else: + logger.error( + f"Failed to save voice after {MAX_RETRY_ATTEMPTS} " + f"attempts: {e}" + ) + raise def format_duration(seconds: float) -> str: diff --git a/src/config.py b/src/config.py index 6c096c1..14da624 100644 --- a/src/config.py +++ b/src/config.py @@ -24,3 +24,12 @@ DATA_PARTIAL_DIR: Path = BASE_DIR / "data_partial" SCENARIOS_DIR: Path = BASE_DIR / "scenarios" DB_DIR: Path = BASE_DIR / "db" DB_PATH: Path = DB_DIR / "bot.db" + +# Таймауты для Telegram API (в секундах) +TELEGRAM_CONNECT_TIMEOUT: int = 10 +TELEGRAM_READ_TIMEOUT: int = 30 +TELEGRAM_WRITE_TIMEOUT: int = 30 + +# Настройки retry +MAX_RETRY_ATTEMPTS: int = 3 +RETRY_DELAYS: tuple[float, ...] = (1.0, 2.0, 4.0) diff --git a/src/handlers.py b/src/handlers.py index d4b6035..6aafaea 100644 --- a/src/handlers.py +++ b/src/handlers.py @@ -6,6 +6,7 @@ from telegram import ( ReplyKeyboardRemove, Update, ) +from telegram.error import TelegramError from telegram.ext import ContextTypes from src.config import ADMIN_LOGIN @@ -47,6 +48,7 @@ from src.messages import ( SPECIFY_GENDER_TEXT, TRACK_SAVED_TEXT, VOICE_EXPECTED_TEXT, + VOICE_SAVE_ERROR_TEXT, ) from src.scenarios import find_available_track @@ -869,15 +871,20 @@ async def handle_voice_message( real_replica_index = replicas[session.replica_index].replica_index voice = update.message.voice - await save_voice_message( - context.bot, - voice.file_id, - user.id, - session.scenario_id, - session.speaker_id, - real_replica_index, - max(1, voice.duration), # телеграм возвращает с точностью до секунд - ) + try: + await save_voice_message( + context.bot, + voice.file_id, + user.id, + session.scenario_id, + session.speaker_id, + real_replica_index, + max(1, voice.duration), # телеграм возвращает с точностью до секунд + ) + except TelegramError as e: + logger.error(f"Failed to save voice for user {user.id}: {e}") + await update.message.reply_text(VOICE_SAVE_ERROR_TEXT) + return track_length = get_track_length(session.scenario_id, session.speaker_id) diff --git a/src/messages.py b/src/messages.py index 028522c..6f3f6b6 100644 --- a/src/messages.py +++ b/src/messages.py @@ -69,6 +69,14 @@ REPEAT_REPLICA_TEXT = "🔄 Перезапись реплики {num}/{total}:" VOICE_EXPECTED_TEXT = "❌ Пожалуйста, отправьте голосовое сообщение с озвучкой реплики." +VOICE_SAVE_ERROR_TEXT = """❌ Произошла ошибка при сохранении аудиозаписи. + +Возможные причины: +• Временные проблемы с сетью +• Проблемы с серверами Telegram + +Пожалуйста, попробуйте отправить голосовое сообщение ещё раз.""" + TRACK_SAVED_TEXT = """✅ Дорожка сохранена! 🙏 Спасибо большое за вашу помощь! Если у вас есть желание и возможность, вы можете озвучить ещё несколько дорожек!