mirror of
https://github.com/DerrtSML/qbittorent_bot.git
synced 2025-10-25 20:10:08 +03:00
Update bot.py
This commit is contained in:
parent
d6e935f706
commit
217fd52b34
119
bot.py
119
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()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user