2025-06-23 17:58:41 +03:00

223 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import logging
import os
from datetime import datetime
# Импорты для Telegram Bot API
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
from telegram.ext import (
Application,
CommandHandler,
MessageHandler,
filters,
ContextTypes,
CallbackQueryHandler,
)
# Импорты для qBittorrent API
from qbittorrentapi import Client, APIError
# --- Настройка логирования ---
logging.basicConfig(
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO
)
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")
QBT_USERNAME = os.getenv("QBT_USERNAME")
QBT_PASSWORD = os.getenv("QBT_PASSWORD")
# --- Глобальная переменная для qBittorrent клиента ---
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
try:
# Используем qbittorrentapi: передаем учетные данные прямо в конструктор Client
# host ожидает строку в формате "IP:PORT"
qb = Client(
host=f"{QBT_HOST}:{QBT_PORT}",
username=QBT_USERNAME,
password=QBT_PASSWORD
)
# Проверяем подключение, обращаясь к версии приложения 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.")
qb = None
return False
except Exception as e:
logger.error(f"An unexpected error occurred during qBittorrent connection: {e}")
qb = None
return False
# --- Команда /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-файла, "
"чтобы добавить загрузку.\n"
"Используй /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.")
return
logger.info(f"Received /status command from {update.effective_user.id}")
# Попытка инициализации клиента qBittorrent
if not init_qbittorrent_client():
await update.message.reply_text(
"Не удалось подключиться к qBittorrent. Проверьте переменные окружения и доступность сервера."
)
return
try:
# Получаем информацию о торрентах
torrents = qb.torrents_info()
if not torrents:
await update.message.reply_text("Загрузок не найдено.")
return
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
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 = ""
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)}мин"
elif minutes > 0:
eta_str = f"{int(minutes)}мин {int(seconds)}с"
else:
eta_str = f"{int(seconds)}с"
else:
eta_str = "Завершено"
status_messages.append(
f"📊 *{torrent.name}*\n"
f" Состояние: {torrent.state}\n"
f" Прогресс: {torrent.progress:.2%}\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"
)
except APIError as e:
logger.error(f"Error getting torrent status: {e}")
await update.message.reply_text(f"Ошибка при получении статуса торрентов: {e}")
except Exception as e:
logger.error(f"An unexpected error occurred in status command: {e}")
await update.message.reply_text(f"Произошла непредвиденная ошибка: {e}")
# --- Обработка 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
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. Проверьте переменные окружения и доступность сервера."
)
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")])
reply_markup = InlineKeyboardMarkup(keyboard)
await update.message.reply_text('Выберите категорию для загрузки:', reply_markup=reply_markup)
except APIError as e:
logger.error(f"Error fetching categories: {e}")
await update.message.reply_text(f"Ошибка при получении категорий: {e}")
except Exception as e:
logger.error(f"An unexpected error occurred in handle_url: {e}")
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() # Обязательно ответьте на запрос обратного вызова, чтобы он не зависал
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
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':
try:
# Добавление торрента
qb.torrents_add(urls=torrent_url, category=category)
await query.edit_message_text(f"Торрент успешно добавлен в qBittorrent (Категория: {category or 'Без категории'})!")