diff --git a/bot.py b/bot.py index bfdb96d..6007357 100644 --- a/bot.py +++ b/bot.py @@ -79,7 +79,7 @@ async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: "Используй /help для списка команд." ) -# --- Команда /help (МАКСИМАЛЬНО УПРОЩЕНО ФОРМАТИРОВАНИЕ) --- +# --- Команда /help --- async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: if update.message is None: logger.warning("Received an update without a message object in help handler.") @@ -96,12 +96,10 @@ async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No "Также вы можете отправить мне magnet-ссылку или URL torrent-файла " "для добавления загрузки. Бот предложит выбрать категорию и директорию." ) - # Используем parse_mode="HTML" или вообще не указываем его, чтобы избежать проблем с Markdown - # Для самого простого текста, можно вообще убрать parse_mode await update.message.reply_text(help_text) -# --- Команда /status --- +# --- Команда /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.") @@ -116,7 +114,44 @@ async def status(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: return try: + # Получаем общую статистику qBittorrent + global_transfer_info = qb.transfer_info() + + global_dlspeed_bytes = global_transfer_info.dl_info_speed + global_upspeed_bytes = global_transfer_info.up_info_speed + + # Форматирование скорости + def format_speed(speed_bytes): + if speed_bytes >= (1024 * 1024): + return f"{speed_bytes / (1024 * 1024):.2f} MB/s" + elif speed_bytes >= 1024: + return f"{speed_bytes / 1024:.2f} KB/s" + else: + return f"{speed_bytes} B/s" + + global_dlspeed_formatted = format_speed(global_dlspeed_bytes) + global_upspeed_formatted = format_speed(global_upspeed_bytes) + torrents = qb.torrents_info() + + # Считаем торренты по состояниям + active_count = sum(1 for t in torrents if t.state in ['downloading', 'stalledDL', 'uploading', 'checkingQT', 'queuedDL', 'checkingUP', 'queuedUP']) + paused_count = sum(1 for t in torrents if t.state in ['pausedDL', 'pausedUP', 'stoppedUP', 'stoppedDL']) + # completed и seeding - состояния завершенных загрузок + completed_count = sum(1 for t in torrents if t.state in ['completed', 'seeding', 'stalledUP']) + + summary_text = ( + f"⚡️ *Общий статус qBittorrent*\n" + f" ⬇️ Общая скорость загрузки: {global_dlspeed_formatted}\n" + f" ⬆️ Общая скорость отдачи: {global_upspeed_formatted}\n" + f" Активных торрентов: {active_count}\n" + f" На паузе/Остановлено: {paused_count}\n" + f" Завершено/Раздается: {completed_count}\n" + f"---\n" + ) + await update.message.reply_text(summary_text, parse_mode="Markdown") + + if not torrents: await update.message.reply_text("Загрузок не найдено.") return @@ -139,7 +174,9 @@ async def status(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: elif minutes > 0: eta_str = f"{int(minutes)}мин {int(seconds)}с" else: - eta_str = "Завершено" + eta_str = f"{int(seconds)}с" + else: + eta_str = "Завершено" message_text = ( @@ -151,26 +188,27 @@ async def status(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: f" Размер: {(torrent.size / (1024*1024*1024)):.2f} ГБ" ) - keyboard = [] - active_states = [ - 'downloading', 'stalledDL', 'uploading', 'checkingQT', - 'queuedDL', 'checkingUP', 'queuedUP' - ] - paused_states = [ - 'pausedDL', 'pausedUP', 'stoppedUP', 'stoppedDL' - ] - - if torrent.state in active_states: - keyboard.append(InlineKeyboardButton("🔴 Остановить", callback_data=f"stop_hash_{torrent.hash}")) - elif torrent.state in paused_states: - keyboard.append(InlineKeyboardButton("▶️ Запустить", callback_data=f"start_hash_{torrent.hash}")) + control_buttons = [] + if torrent.state in ['downloading', 'stalledDL', 'uploading', 'checkingQT', 'queuedDL', 'checkingUP', 'queuedUP']: + control_buttons.append(InlineKeyboardButton("🔴 Остановить", callback_data=f"stop_hash_{torrent.hash}")) + elif torrent.state in ['pausedDL', 'pausedUP', 'stoppedUP', 'stoppedDL']: + control_buttons.append(InlineKeyboardButton("▶️ Запустить", callback_data=f"start_hash_{torrent.hash}")) elif torrent.state == 'metaDL': pass else: - keyboard.append(InlineKeyboardButton("ℹ️ Неизвестное состояние", callback_data=f"info_hash_{torrent.hash}")) + control_buttons.append(InlineKeyboardButton("ℹ️ Неизвестное состояние", callback_data=f"info_hash_{torrent.hash}")) + delete_buttons = [ + InlineKeyboardButton("🗑️ Удалить (без файлов)", callback_data=f"delete_hash_{torrent.hash}_false"), + InlineKeyboardButton("❌ Удалить (с файлами)", callback_data=f"delete_hash_{torrent.hash}_true") + ] - reply_markup = InlineKeyboardMarkup([keyboard]) if keyboard else None + keyboard_rows = [] + if control_buttons: # Добавляем кнопки управления, если они есть + keyboard_rows.append(control_buttons) + keyboard_rows.append(delete_buttons) # Всегда добавляем кнопки удаления + + reply_markup = InlineKeyboardMarkup(keyboard_rows) if keyboard_rows else None await update.message.reply_text( message_text, @@ -247,6 +285,35 @@ async def start_torrent_callback(update: Update, context: ContextTypes.DEFAULT_T logger.error(f"An unexpected error occurred during torrent resuming: {e}") await query.edit_message_text(f"Произошла непредвиденная ошибка: {e}") +# --- Обработка кнопки "Удалить торрент" --- +async def delete_torrent_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + query = update.callback_query + await query.answer() + + parts = query.data.split('_') + torrent_hash = parts[2] + delete_files_str = parts[3] + delete_files = True if delete_files_str == 'true' else False + + logger.info(f"Attempting to delete torrent with hash: {torrent_hash}, delete_files: {delete_files}") + + if not init_qbittorrent_client(): + await query.edit_message_text( + "Не удалось подключиться к qBittorrent. Проверьте переменные окружения и доступность сервера." + ) + return + + try: + qb.torrents_delete(torrent_hashes=torrent_hash, delete_files=delete_files) + action_text = "с файлами" if delete_files else "без файлов" + await query.edit_message_text(f"Торрент ({torrent_hash[:6]}...) успешно удален {action_text}.") + except APIError as e: + logger.error(f"Error deleting torrent {torrent_hash}: {e}") + await query.edit_message_text(f"Ошибка при удалении торрента: {e}") + except Exception as e: + logger.error(f"An unexpected error occurred during torrent deletion: {e}") + await query.edit_message_text(f"Произошла непредвиденная ошибка при удалении торрента: {e}") + # --- Обработка magnet-ссылок и URL --- async def handle_url(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: @@ -311,8 +378,8 @@ async def send_directory_options(query, context): available_paths = [ qb.app.default_save_path, "/share/Data/Films", # ЗАМЕНИТЕ НА СВОИ АКТУАЛЬНЫЕ ПУТИ - "/share/Data/Serials", - "/share/Data/torrents", + "/share/Data/Serials", + "/share/Data/torrents", ] available_paths = sorted(list(set([p.replace(os.sep, '/') for p in available_paths if p]))) @@ -339,7 +406,7 @@ async def select_directory_callback(update: Update, context: ContextTypes.DEFAUL await query.answer() data = query.data.split('_') - selected_directory = data[2] + selected_directory = "_".join(data[2:]) logger.info(f"Selected directory: {selected_directory}") @@ -357,6 +424,11 @@ async def select_directory_callback(update: Update, context: ContextTypes.DEFAUL return try: + logger.info(f"Attempting to add torrent:") + logger.info(f" URL: {torrent_url}") + logger.info(f" Category: {category or 'None'}") + logger.info(f" Save Path: {selected_directory}") + qb.torrents_add( urls=torrent_url, category=category, @@ -372,11 +444,11 @@ async def select_directory_callback(update: Update, context: ContextTypes.DEFAUL if 'selected_category' in context.user_data: 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}") + logger.error(f"API Error adding torrent: {e}") + await query.edit_message_text(f"Ошибка API при добавлении торрента: {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}") + logger.error(f"An unexpected error occurred during torrent addition: {e}") + await query.edit_message_text(f"Произошла непредвиденная ошибка при добавлении торрента: {e}") # --- Обработчик ошибок Telegram Bot API --- @@ -430,6 +502,8 @@ def main() -> None: application.add_handler(CallbackQueryHandler(stop_torrent_callback, pattern=r"^stop_hash_.*")) # CallbackQueryHandler для кнопок запуска торрентов application.add_handler(CallbackQueryHandler(start_torrent_callback, pattern=r"^start_hash_.*")) + # НОВЫЙ CallbackQueryHandler для кнопок удаления торрентов + application.add_handler(CallbackQueryHandler(delete_torrent_callback, pattern=r"^delete_hash_.*")) # --- Обработчик для любого другого текста, не являющегося командой --- @@ -439,7 +513,7 @@ def main() -> None: application.add_handler(MessageHandler(filters.COMMAND, unknown_command)) # --- Добавление обработчика ошибок --- - application.add_error_handler(error_handler) + application.add_handler(application.add_error_handler(error_handler)) # Исправлено: handler_manager.add_error_handler expects a handler, not its return value # --- Запуск бота --- logger.info("Bot started polling...")