From 217fd52b344f49b8b7791162fc7f4edfc6c23853 Mon Sep 17 00:00:00 2001 From: DerrtSML <93052047+DerrtSML@users.noreply.github.com> Date: Mon, 23 Jun 2025 17:58:41 +0300 Subject: [PATCH] Update bot.py --- bot.py | 119 +++++++++++++++++---------------------------------------- 1 file changed, 36 insertions(+), 83 deletions(-) diff --git a/bot.py b/bot.py index 02961a6..ecc2f75 100644 --- a/bot.py +++ b/bot.py @@ -23,6 +23,7 @@ logging.basicConfig( logger = logging.getLogger(__name__) # --- Переменные окружения для qBittorrent --- +# Важно: эти переменные должны быть установлены в Portainer TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN") QBT_HOST = os.getenv("QBT_HOST") QBT_PORT = os.getenv("QBT_PORT") @@ -35,6 +36,7 @@ qb = None # --- Инициализация qBittorrent клиента --- def init_qbittorrent_client(): global qb + # Проверяем, что все необходимые переменные окружения установлены if not all([QBT_HOST, QBT_PORT, QBT_USERNAME, QBT_PASSWORD]): logger.error("QBittorrent credentials (host, port, username, password) are not fully set in environment variables.") return False @@ -46,10 +48,10 @@ def init_qbittorrent_client(): username=QBT_USERNAME, password=QBT_PASSWORD ) - # Проверяем подключение, вызывая что-нибудь простое, например, api_version - # Это также выполняет аутентификацию - qb.app.api_version - logger.info(f"Successfully connected to qBittorrent at {QBT_HOST}:{QBT_PORT}") + # Проверяем подключение, обращаясь к версии приложения qBittorrent. + # Это неявно проверяет соединение и аутентификацию. + qb_version = qb.app.version + logger.info(f"Connected to qBittorrent API v{qb_version} on {QBT_HOST}:{QBT_PORT}") return True except APIError as e: logger.error(f"Failed to connect or login to qBittorrent: {e}. Check your qBittorrent Web UI address and credentials.") @@ -62,18 +64,21 @@ def init_qbittorrent_client(): # --- Команда /start --- async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + # Защитная проверка на наличие объекта сообщения if update.message is None: logger.warning("Received an update without a message object in start handler.") return logger.info(f"Received /start command from {update.effective_user.id}") + # Попытка инициализации клиента qBittorrent if not init_qbittorrent_client(): await update.message.reply_text( "Не удалось подключиться к qBittorrent. Проверьте переменные окружения и доступность сервера." ) return + # Отправка приветственного сообщения await update.message.reply_text( "Привет! Я бот для управления qBittorrent.\n" "Отправь мне magnet-ссылку или URL torrent-файла, " @@ -83,12 +88,14 @@ async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: # --- Команда /status --- async def status(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + # Защитная проверка на наличие объекта сообщения if update.message is None: logger.warning("Received an update without a message object in status handler.") return logger.info(f"Received /status command from {update.effective_user.id}") + # Попытка инициализации клиента qBittorrent if not init_qbittorrent_client(): await update.message.reply_text( "Не удалось подключиться к qBittorrent. Проверьте переменные окружения и доступность сервера." @@ -96,6 +103,7 @@ async def status(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: return try: + # Получаем информацию о торрентах torrents = qb.torrents_info() if not torrents: await update.message.reply_text("Загрузок не найдено.") @@ -104,21 +112,21 @@ async def status(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: status_messages = [] for torrent in torrents: # Преобразование скорости из B/s в KB/s или MB/s - download_speed = torrent.dlspeed / (1024 * 1024) if torrent.dlspeed > (1024 * 1024) else torrent.dlspeed / 1024 - upload_speed = torrent.upspeed / (1024 * 1024) if torrent.upspeed > (1024 * 1024) else torrent.upspeed / 1024 + download_speed = torrent.dlspeed / (1024 * 1024) if torrent.dlspeed >= (1024 * 1024) else torrent.dlspeed / 1024 + upload_speed = torrent.upspeed / (1024 * 1024) if torrent.upspeed >= (1024 * 1024) else torrent.upspeed / 1024 - dl_unit = "MB/s" if torrent.dlspeed > (1024 * 1024) else "KB/s" - up_unit = "MB/s" if torrent.upspeed > (1024 * 1024) else "KB/s" + dl_unit = "MB/s" if torrent.dlspeed >= (1024 * 1024) else "KB/s" + up_unit = "MB/s" if torrent.upspeed >= (1024 * 1024) else "KB/s" # Форматирование времени выполнения (eta - Estimated Time of Arrival) - eta_str = f"{torrent.eta} сек." + eta_str = "" if torrent.eta == 8640000: # qBittorrent's way of saying infinite eta_str = "∞" elif torrent.eta > 0: hours, remainder = divmod(torrent.eta, 3600) minutes, seconds = divmod(remainder, 60) if hours > 0: - eta_str = f"{int(hours)}ч {int(minutes)}мин {int(seconds)}с" + eta_str = f"{int(hours)}ч {int(minutes)}мин" elif minutes > 0: eta_str = f"{int(minutes)}мин {int(seconds)}с" else: @@ -131,12 +139,12 @@ async def status(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: f"📊 *{torrent.name}*\n" f" Состояние: {torrent.state}\n" f" Прогресс: {torrent.progress:.2%}\n" - f" Скачивание: {download_speed:.2f} {dl_unit}\n" - f" Отдача: {upload_speed:.2f} {up_unit}\n" + f" ⬇️ {download_speed:.2f} {dl_unit} ⬆️ {upload_speed:.2f} {up_unit}\n" f" ETA: {eta_str}\n" f" Размер: {(torrent.size / (1024*1024*1024)):.2f} ГБ" # Convert bytes to GB ) + # Отправляем сообщения о статусе торрентов await update.message.reply_text( "\n\n".join(status_messages), parse_mode="Markdown" ) @@ -150,6 +158,7 @@ async def status(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: # --- Обработка magnet-ссылок и URL --- async def handle_url(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + # Защитная проверка на наличие объекта сообщения if update.message is None: logger.warning("Received an update without a message object in handle_url handler.") return @@ -157,6 +166,7 @@ async def handle_url(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None text = update.message.text logger.info(f"Received URL/Magnet: {text} from {update.effective_user.id}") + # Попытка инициализации клиента qBittorrent if not init_qbittorrent_client(): await update.message.reply_text( "Не удалось подключиться к qBittorrent. Проверьте переменные окружения и доступность сервера." @@ -164,10 +174,11 @@ async def handle_url(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None return try: - # Получаем список категорий + # Получаем список категорий из qBittorrent categories = qb.categories.info() keyboard = [] for category_name in categories.keys(): + # Формируем данные для кнопки: "add_URL_CATEGORYNAME" keyboard.append([InlineKeyboardButton(category_name, callback_data=f"add_{text}_{category_name}")]) # Добавляем кнопку для "Без категории" @@ -184,86 +195,28 @@ async def handle_url(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None await update.message.reply_text(f"Произошла непредвиденная ошибка: {e}") +# --- Обработка нажатий Inline-кнопок --- async def button_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: query = update.callback_query - await query.answer() # Обязательно ответьте на запрос обратного вызова + await query.answer() # Обязательно ответьте на запрос обратного вызова, чтобы он не зависал data = query.data.split('_') action = data[0] torrent_url = data[1] - category = data[2] if len(data) > 2 else None # 'no_category' или имя категории + # Если категория не указана (например, "no_category"), устанавливаем ее в None + category = data[2] if len(data) > 2 and data[2] != 'no_category' else None - if category == 'no_category': - category = None + logger.info(f"Button callback - Action: {action}, URL: {torrent_url}, Category: {category}") + + # Попытка инициализации клиента qBittorrent + if not init_qbittorrent_client(): + await query.edit_message_text( + "Не удалось подключиться к qBittorrent. Проверьте переменные окружения и доступность сервера." + ) + return if action == 'add': - logger.info(f"Adding torrent: {torrent_url} to category: {category}") try: # Добавление торрента qb.torrents_add(urls=torrent_url, category=category) await query.edit_message_text(f"Торрент успешно добавлен в qBittorrent (Категория: {category or 'Без категории'})!") - except APIError as e: - logger.error(f"Error adding torrent: {e}") - await query.edit_message_text(f"Ошибка при добавлении торрента: {e}") - except Exception as e: - logger.error(f"An unexpected error occurred during torrent addition: {e}") - await query.edit_message_text(f"Произошла непредвиденная ошибка: {e}") - - -# --- Обработчик ошибок --- -async def error_handler(update: object, context: ContextTypes.DEFAULT_TYPE) -> None: - logger.error(f"Error occurred: {context.error}") - if update.effective_message: - await update.effective_message.reply_text(f"Произошла внутренняя ошибка: {context.error}") - -# --- Обработчик для неизвестных команд --- -async def unknown_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: - if update.message is None: - logger.warning("Received an unknown command update without a message object.") - return - logger.info(f"Received unknown command: {update.message.text} from {update.effective_user.id}") - await update.message.reply_text("Извините, я не понял эту команду.") - -# --- Обработчик для любого другого текста (для отладки) --- -async def echo(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: - if update.message is None: - logger.warning(f"Received non-text update in echo handler: {update}") - return - logger.info(f"Received non-command text: {update.message.text} from {update.effective_user.id}") - await update.message.reply_text(f"Вы сказали: {update.message.text}") - - -# --- Основная функция --- -def main() -> None: - if not TELEGRAM_BOT_TOKEN: - logger.critical("TELEGRAM_BOT_TOKEN environment variable is not set. Exiting.") - exit(1) - - application = Application.builder().token(TELEGRAM_BOT_TOKEN).build() - - # --- Добавление обработчиков команд --- - application.add_handler(CommandHandler("start", start)) - application.add_handler(CommandHandler("status", status)) - - # --- Добавление обработчиков сообщений --- - # Перехватывает URL и Magnet-ссылки - application.add_handler(MessageHandler(filters.TEXT & (filters.Regex(r"magnet:\?xt=urn:[a-z0-9]+") | filters.Regex(r"https?://[^\s]+(?:\.torrent|\/torrent\.php\?hash=)[\S]*")), handle_url)) - - # Добавьте CallbackQueryHandler для кнопок - application.add_handler(CallbackQueryHandler(button_callback)) - - # --- Обработчик для любого другого текста, не являющегося командой (для отладки) --- - application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, echo)) - - # --- Обработчик для неизвестных команд (должен быть последним) --- - application.add_handler(MessageHandler(filters.COMMAND, unknown_command)) - - # --- Добавление обработчика ошибок --- - application.add_error_handler(error_handler) - - # --- Запуск бота --- - logger.info("Bot started polling...") - application.run_polling(allowed_updates=Update.ALL_TYPES) - -if __name__ == "__main__": - main()