From 2808b353101460027dee9a3ab32a2d3fc0d22d8a Mon Sep 17 00:00:00 2001 From: DerrtSML <93052047+DerrtSML@users.noreply.github.com> Date: Mon, 23 Jun 2025 18:17:15 +0300 Subject: [PATCH] Update bot.py --- bot.py | 204 ++++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 181 insertions(+), 23 deletions(-) diff --git a/bot.py b/bot.py index ecc2f75..514ec09 100644 --- a/bot.py +++ b/bot.py @@ -174,17 +174,18 @@ 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}")]) - - # Добавляем кнопку для "Без категории" - keyboard.append([InlineKeyboardButton("Без категории", callback_data=f"add_{text}_no_category")]) + # Сохраняем URL торрента в user_data контекста, чтобы использовать его позже + context.user_data['current_torrent_url'] = text - reply_markup = InlineKeyboardMarkup(keyboard) + # Запрашиваем категории + categories = qb.categories.info() + category_keyboard = [] + for category_name in categories.keys(): + category_keyboard.append([InlineKeyboardButton(category_name, callback_data=f"select_category_{category_name}")]) + + category_keyboard.append([InlineKeyboardButton("Без категории", callback_data="select_category_no_category")]) + + reply_markup = InlineKeyboardMarkup(category_keyboard) await update.message.reply_text('Выберите категорию для загрузки:', reply_markup=reply_markup) except APIError as e: @@ -195,18 +196,88 @@ 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: +# --- Обработка выбора категории --- +async def select_category_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] - # Если категория не указана (например, "no_category"), устанавливаем ее в None - category = data[2] if len(data) > 2 and data[2] != 'no_category' else None + # action = data[0] # select + # type = data[1] # category + selected_category = data[2] if len(data) > 2 and data[2] != 'no_category' else None - logger.info(f"Button callback - Action: {action}, URL: {torrent_url}, Category: {category}") + logger.info(f"Selected category: {selected_category}") + + # Сохраняем выбранную категорию + context.user_data['selected_category'] = selected_category + + # Теперь предложим выбор директорий + await query.edit_message_text("Выберите директорию загрузки:") + await send_directory_options(query, context) + +# --- Отправка опций директорий --- +async def send_directory_options(query, context): + try: + # Получаем пути сохранения из qBittorrent + # qbittorrentapi.app.default_save_path - это дефолтная директория + # qbittorrentapi.app.preferences().save_path - это та же дефолтная директория, + # но если хотим показать другие, нам нужно их получить откуда-то еще, + # например, из существующих торрентов или заранее заданного списка. + # Для простоты пока используем дефолтный и какой-нибудь пример. + + # !!! ВНИМАНИЕ: qBittorrent API не предоставляет список "доступных" директорий автоматически. + # Вам нужно будет вручную указать те директории, которые вы хотите предложить пользователю. + # Здесь приведен ПРИМЕР, как это можно сделать. + # ЗАМЕНИТЕ ЭТОТ СПИСОК СВОИМИ АКТУАЛЬНЫМИ ПУТЯМИ! + available_paths = [ + qb.app.default_save_path, # Дефолтный путь qBittorrent + "/downloads/movies", + "/downloads/series", + "/downloads/other", + # Добавьте здесь другие пути, если они у вас есть + ] + + # Удаляем дубликаты и пустые пути + available_paths = list(set([p for p in available_paths if p])) + + directory_keyboard = [] + for path in available_paths: + # path.replace(os.sep, '/') для кроссплатформенности и читаемости + display_path = path.replace(os.sep, '/') + directory_keyboard.append([InlineKeyboardButton(display_path, callback_data=f"select_dir_{path}")]) + + # Опция для использования дефолтной директории qBittorrent (если пользователь не хочет выбирать) + directory_keyboard.append([InlineKeyboardButton("Использовать дефолтную qBittorrent", callback_data=f"select_dir_{qb.app.default_save_path}")]) + + reply_markup = InlineKeyboardMarkup(directory_keyboard) + await query.edit_message_reply_markup(reply_markup=reply_markup) # Изменяем только разметку, чтобы сообщение осталось + + except APIError as e: + logger.error(f"Error fetching default save path: {e}") + await query.edit_message_text(f"Ошибка при получении директорий сохранения: {e}") + except Exception as e: + logger.error(f"An unexpected error occurred in send_directory_options: {e}") + await query.edit_message_text(f"Произошла непредвиденная ошибка: {e}") + + +# --- Обработка выбора директории и добавление торрента --- +async def select_directory_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + query = update.callback_query + await query.answer() + + data = query.data.split('_') + # action = data[0] # select + # type = data[1] # dir + selected_directory = data[2] # выбранный путь + + logger.info(f"Selected directory: {selected_directory}") + + torrent_url = context.user_data.get('current_torrent_url') + category = context.user_data.get('selected_category') + + if not torrent_url: + await query.edit_message_text("Ошибка: URL торрента не найден. Пожалуйста, попробуйте снова.") + return # Попытка инициализации клиента qBittorrent if not init_qbittorrent_client(): @@ -215,8 +286,95 @@ async def button_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> ) return - if action == 'add': - try: - # Добавление торрента - qb.torrents_add(urls=torrent_url, category=category) - await query.edit_message_text(f"Торрент успешно добавлен в qBittorrent (Категория: {category or 'Без категории'})!") + try: + # Добавление торрента с выбранной категорией и директорией + qb.torrents_add( + urls=torrent_url, + category=category, + save_path=selected_directory # Указываем директорию сохранения + ) + await query.edit_message_text( + f"Торрент успешно добавлен в qBittorrent.\n" + f"Категория: {category or 'Без категории'}\n" + f"Директория: {selected_directory}" + ) + # Очищаем данные из context.user_data после использования + del context.user_data['current_torrent_url'] + del context.user_data['selected_category'] + except APIError as e: + logger.error(f"Error adding torrent with path: {e}") + await query.edit_message_text(f"Ошибка при добавлении торрента: {e}") + except Exception as e: + logger.error(f"An unexpected error occurred during torrent addition with path: {e}") + await query.edit_message_text(f"Произошла непредвиденная ошибка: {e}") + + +# --- Обработчик ошибок Telegram Bot API --- +async def error_handler(update: object, context: ContextTypes.DEFAULT_TYPE) -> None: + logger.error(f"Exception while handling an update: {context.error}") + if update and 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-ссылки + url_regex = r"magnet:\?xt=urn:[a-z0-9]+" # Регулярное выражение для magnet-ссылок + torrent_url_regex = r"https?://[^\s]+(?:\.torrent|\/torrent\.php\?hash=)[\S]*" # Регулярное выражение для URL торрент-файлов + + application.add_handler(MessageHandler(filters.TEXT & (filters.Regex(url_regex) | filters.Regex(torrent_url_regex)), handle_url)) + + # Добавляем CallbackQueryHandler для кнопок выбора категории + application.add_handler(CallbackQueryHandler(select_category_callback, pattern=r"^select_category_.*")) + # Добавляем CallbackQueryHandler для кнопок выбора директории + application.add_handler(CallbackQueryHandler(select_directory_callback, pattern=r"^select_dir_.*")) + + + # --- Обработчик для любого другого текста, не являющегося командой (для отладки) --- + # Должен быть перед обработчиком неизвестных команд + 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...") + # allowed_updates=Update.ALL_TYPES помогает убедиться, что бот получает все типы обновлений, + # что полезно для отладки, но обычно можно сузить список для продакшена. + application.run_polling(allowed_updates=Update.ALL_TYPES) + +if __name__ == "__main__": + main()