mirror of
https://github.com/DerrtSML/qbittorent_bot.git
synced 2025-10-26 04:20:08 +03:00
Update bot.py
This commit is contained in:
parent
739bf44352
commit
d6e935f706
279
bot.py
279
bot.py
@ -1,67 +1,58 @@
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# Импорты для Telegram Bot API
|
||||||
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
|
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
|
||||||
from telegram.ext import (
|
from telegram.ext import (
|
||||||
Application,
|
Application,
|
||||||
CommandHandler,
|
CommandHandler,
|
||||||
MessageHandler,
|
MessageHandler,
|
||||||
filters,
|
filters,
|
||||||
CallbackQueryHandler,
|
|
||||||
ContextTypes,
|
ContextTypes,
|
||||||
|
CallbackQueryHandler,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Импорты для qBittorrent API
|
||||||
from qbittorrentapi import Client, APIError
|
from qbittorrentapi import Client, APIError
|
||||||
|
|
||||||
# Настройка логирования
|
# --- Настройка логирования ---
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO
|
||||||
level=logging.INFO
|
|
||||||
)
|
)
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# --- Конфигурация (будет загружаться из переменных окружения Docker) ---
|
# --- Переменные окружения для qBittorrent ---
|
||||||
TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
|
TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
|
||||||
QBT_HOST = os.getenv("QBT_HOST")
|
QBT_HOST = os.getenv("QBT_HOST")
|
||||||
QBT_PORT = os.getenv("QBT_PORT", "8080")
|
QBT_PORT = os.getenv("QBT_PORT")
|
||||||
QBT_USERNAME = os.getenv("QBT_USERNAME")
|
QBT_USERNAME = os.getenv("QBT_USERNAME")
|
||||||
QBT_PASSWORD = os.getenv("QBT_PASSWORD")
|
QBT_PASSWORD = os.getenv("QBT_PASSWORD")
|
||||||
|
|
||||||
# Определение доступных директорий (предполагаем, что они известны)
|
# --- Глобальная переменная для qBittorrent клиента ---
|
||||||
# В реальной системе можно было бы получить список из qBittorrent API
|
|
||||||
# или настроить их через переменные окружения.
|
|
||||||
DOWNLOAD_DIRECTORIES = {
|
|
||||||
"Фильмы": "/share/Data/Felms",
|
|
||||||
"Сериалы": "/share/Data/Serials",
|
|
||||||
"Музыка": "/share/Music",
|
|
||||||
"Другое": "/share/Data/torrents",
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Инициализация qBittorrent клиента ---
|
|
||||||
qb = None
|
qb = None
|
||||||
|
|
||||||
|
# --- Инициализация qBittorrent клиента ---
|
||||||
def init_qbittorrent_client():
|
def init_qbittorrent_client():
|
||||||
global qb
|
global qb
|
||||||
# This check is still good, it ensures env vars are set
|
if not all([QBT_HOST, QBT_PORT, QBT_USERNAME, QBT_PASSWORD]):
|
||||||
if not all([QBT_HOST, QBT_USERNAME, QBT_PASSWORD]):
|
logger.error("QBittorrent credentials (host, port, username, password) are not fully set in environment variables.")
|
||||||
logger.error("QBittorrent credentials are not fully set in environment variables.")
|
|
||||||
return False
|
return False
|
||||||
try:
|
try:
|
||||||
# --- ИСПОЛЬЗУЙТЕ ПЕРЕМЕННЫЕ ОКРУЖЕНИЯ ЗДЕСЬ ---
|
# Используем qbittorrentapi: передаем учетные данные прямо в конструктор Client
|
||||||
# QBT_HOST и QBT_PORT уже должны быть строками из env,
|
# host ожидает строку в формате "IP:PORT"
|
||||||
# так что f-string корректно объединит их.
|
|
||||||
# QBT_USERNAME и QBT_PASSWORD также должны быть строками из env.
|
|
||||||
qb = Client(
|
qb = Client(
|
||||||
host=f"{QBT_HOST}:{QBT_PORT}", # Используем QBT_HOST и QBT_PORT из переменных окружения
|
host=f"{QBT_HOST}:{QBT_PORT}",
|
||||||
username=QBT_USERNAME, # Используем QBT_USERNAME из переменных окружения
|
username=QBT_USERNAME,
|
||||||
password=QBT_PASSWORD # Используем QBT_PASSWORD из переменных окружения
|
password=QBT_PASSWORD
|
||||||
)
|
)
|
||||||
# Проверим подключение, вызвав что-нибудь простое, например, api_version
|
# Проверяем подключение, вызывая что-нибудь простое, например, api_version
|
||||||
# Это также выполняет аутентификацию
|
# Это также выполняет аутентификацию
|
||||||
qb.app.api_version
|
qb.app.api_version
|
||||||
|
|
||||||
logger.info(f"Successfully connected to qBittorrent at {QBT_HOST}:{QBT_PORT}")
|
logger.info(f"Successfully connected to qBittorrent at {QBT_HOST}:{QBT_PORT}")
|
||||||
return True
|
return True
|
||||||
except APIError as e:
|
except APIError as e:
|
||||||
logger.error(f"Failed to connect or login to qBittorrent: {e}")
|
logger.error(f"Failed to connect or login to qBittorrent: {e}. Check your qBittorrent Web UI address and credentials.")
|
||||||
qb = None
|
qb = None
|
||||||
return False
|
return False
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -69,19 +60,20 @@ def init_qbittorrent_client():
|
|||||||
qb = None
|
qb = None
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# --- Обработчики команд ---
|
# --- Команда /start ---
|
||||||
|
|
||||||
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
# ДОБАВЬТЕ ЭТУ ПРОВЕРКУ
|
|
||||||
if update.message is None:
|
if update.message is None:
|
||||||
logger.warning("Received an update without a message object in start handler.")
|
logger.warning("Received an update without a message object in start handler.")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
logger.info(f"Received /start command from {update.effective_user.id}")
|
||||||
|
|
||||||
if not init_qbittorrent_client():
|
if not init_qbittorrent_client():
|
||||||
await update.message.reply_text(
|
await update.message.reply_text(
|
||||||
"Не удалось подключиться к qBittorrent. Проверьте переменные окружения и доступность сервера."
|
"Не удалось подключиться к qBittorrent. Проверьте переменные окружения и доступность сервера."
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
await update.message.reply_text(
|
await update.message.reply_text(
|
||||||
"Привет! Я бот для управления qBittorrent.\n"
|
"Привет! Я бот для управления qBittorrent.\n"
|
||||||
"Отправь мне magnet-ссылку или URL torrent-файла, "
|
"Отправь мне magnet-ссылку или URL torrent-файла, "
|
||||||
@ -89,110 +81,189 @@ async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|||||||
"Используй /status для просмотра текущих загрузок."
|
"Используй /status для просмотра текущих загрузок."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# --- Команда /status ---
|
||||||
async def status(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
async def status(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
if not qb:
|
if update.message is None:
|
||||||
if not init_qbittorrent_client():
|
logger.warning("Received an update without a message object in status handler.")
|
||||||
await update.message.reply_text(
|
return
|
||||||
"Не удалось подключиться к qBittorrent. Попробуйте еще раз или проверьте настройки."
|
|
||||||
)
|
logger.info(f"Received /status command from {update.effective_user.id}")
|
||||||
return
|
|
||||||
|
if not init_qbittorrent_client():
|
||||||
|
await update.message.reply_text(
|
||||||
|
"Не удалось подключиться к qBittorrent. Проверьте переменные окружения и доступность сервера."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
torrents = qb.torrents()
|
torrents = qb.torrents_info()
|
||||||
if not torrents:
|
if not torrents:
|
||||||
await update.message.reply_text("Нет активных загрузок.")
|
await update.message.reply_text("Загрузок не найдено.")
|
||||||
return
|
return
|
||||||
|
|
||||||
message = "Текущие загрузки:\n\n"
|
status_messages = []
|
||||||
for t in torrents:
|
for torrent in torrents:
|
||||||
progress = f"{t.progress:.2%}"
|
# Преобразование скорости из B/s в KB/s или MB/s
|
||||||
size = f"{t.size / (1024*1024*1024):.2f} GB" if t.size else "N/A"
|
download_speed = torrent.dlspeed / (1024 * 1024) if torrent.dlspeed > (1024 * 1024) else torrent.dlspeed / 1024
|
||||||
download_speed = f"{t.dlspeed / (1024*1024):.2f} MB/s"
|
upload_speed = torrent.upspeed / (1024 * 1024) if torrent.upspeed > (1024 * 1024) else torrent.upspeed / 1024
|
||||||
upload_speed = f"{t.upspeed / (1024*1024):.2f} MB/s"
|
|
||||||
|
|
||||||
message += (
|
dl_unit = "MB/s" if torrent.dlspeed > (1024 * 1024) else "KB/s"
|
||||||
f"📝 Имя: {t.name}\n"
|
up_unit = "MB/s" if torrent.upspeed > (1024 * 1024) else "KB/s"
|
||||||
f"📊 Прогресс: {progress}\n"
|
|
||||||
f"📦 Размер: {size}\n"
|
# Форматирование времени выполнения (eta - Estimated Time of Arrival)
|
||||||
f"⬇️ Скорость загрузки: {download_speed}\n"
|
eta_str = f"{torrent.eta} сек."
|
||||||
f"⬆️ Скорость отдачи: {upload_speed}\n"
|
if torrent.eta == 8640000: # qBittorrent's way of saying infinite
|
||||||
f"🚦 Статус: {t.state}\n"
|
eta_str = "∞"
|
||||||
f"--- \n"
|
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)}с"
|
||||||
|
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}\n"
|
||||||
|
f" Отдача: {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(message)
|
|
||||||
|
await update.message.reply_text(
|
||||||
|
"\n\n".join(status_messages), parse_mode="Markdown"
|
||||||
|
)
|
||||||
|
|
||||||
except APIError as e:
|
except APIError as e:
|
||||||
logger.error(f"Error fetching torrents: {e}")
|
logger.error(f"Error getting torrent status: {e}")
|
||||||
await update.message.reply_text(f"Ошибка при получении списка загрузок: {e}")
|
await update.message.reply_text(f"Ошибка при получении статуса торрентов: {e}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"An unexpected error occurred: {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}")
|
||||||
|
|
||||||
|
if not init_qbittorrent_client():
|
||||||
|
await update.message.reply_text(
|
||||||
|
"Не удалось подключиться к qBittorrent. Проверьте переменные окружения и доступность сервера."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Получаем список категорий
|
||||||
|
categories = qb.categories.info()
|
||||||
|
keyboard = []
|
||||||
|
for category_name in categories.keys():
|
||||||
|
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}")
|
await update.message.reply_text(f"Произошла непредвиденная ошибка: {e}")
|
||||||
|
|
||||||
|
|
||||||
async def handle_url(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
||||||
if not qb:
|
|
||||||
if not init_qbittorrent_client():
|
|
||||||
await update.message.reply_text(
|
|
||||||
"Не удалось подключиться к qBittorrent. Попробуйте еще раз или проверьте настройки."
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
url = update.message.text
|
|
||||||
context.user_data['download_url'] = url
|
|
||||||
|
|
||||||
keyboard = []
|
|
||||||
for name, path in DOWNLOAD_DIRECTORIES.items():
|
|
||||||
keyboard.append([InlineKeyboardButton(name, callback_data=f"dir_{path}")])
|
|
||||||
reply_markup = InlineKeyboardMarkup(keyboard)
|
|
||||||
|
|
||||||
await update.message.reply_text(
|
|
||||||
f"Вы хотите загрузить: `{url}`\n"
|
|
||||||
"Выберите директорию для загрузки:",
|
|
||||||
reply_markup=reply_markup,
|
|
||||||
parse_mode='Markdown'
|
|
||||||
)
|
|
||||||
|
|
||||||
async def button_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
async def button_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
query = update.callback_query
|
query = update.callback_query
|
||||||
await query.answer()
|
await query.answer() # Обязательно ответьте на запрос обратного вызова
|
||||||
|
|
||||||
if query.data.startswith("dir_"):
|
data = query.data.split('_')
|
||||||
selected_directory = query.data.replace("dir_", "")
|
action = data[0]
|
||||||
download_url = context.user_data.get('download_url')
|
torrent_url = data[1]
|
||||||
|
category = data[2] if len(data) > 2 else None # 'no_category' или имя категории
|
||||||
|
|
||||||
if not download_url:
|
if category == 'no_category':
|
||||||
await query.edit_message_text("Ошибка: URL для загрузки не найден. Пожалуйста, отправьте ссылку снова.")
|
category = None
|
||||||
return
|
|
||||||
|
|
||||||
|
if action == 'add':
|
||||||
|
logger.info(f"Adding torrent: {torrent_url} to category: {category}")
|
||||||
try:
|
try:
|
||||||
qb.download_from_link(download_url, save_path=selected_directory)
|
# Добавление торрента
|
||||||
await query.edit_message_text(
|
qb.torrents_add(urls=torrent_url, category=category)
|
||||||
f"Загрузка '{download_url}' добавлена в '{selected_directory}'."
|
await query.edit_message_text(f"Торрент успешно добавлен в qBittorrent (Категория: {category or 'Без категории'})!")
|
||||||
)
|
|
||||||
logger.info(f"Added download '{download_url}' to '{selected_directory}'")
|
|
||||||
except APIError as e:
|
except APIError as e:
|
||||||
logger.error(f"Error adding download: {e}")
|
logger.error(f"Error adding torrent: {e}")
|
||||||
await query.edit_message_text(f"Ошибка при добавлении загрузки: {e}")
|
await query.edit_message_text(f"Ошибка при добавлении торрента: {e}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"An unexpected error occurred while adding download: {e}")
|
logger.error(f"An unexpected error occurred during torrent addition: {e}")
|
||||||
await query.edit_message_text(f"Произошла непредвиденная ошибка при добавлении загрузки: {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:
|
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 = Application.builder().token(TELEGRAM_BOT_TOKEN).build()
|
||||||
|
|
||||||
|
# --- Добавление обработчиков команд ---
|
||||||
application.add_handler(CommandHandler("start", start))
|
application.add_handler(CommandHandler("start", start))
|
||||||
application.add_handler(CommandHandler("status", status))
|
application.add_handler(CommandHandler("status", status))
|
||||||
application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_url))
|
|
||||||
|
# --- Добавление обработчиков сообщений ---
|
||||||
|
# Перехватывает 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(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...")
|
logger.info("Bot started polling...")
|
||||||
application.run_polling(allowed_updates=Update.ALL_TYPES)
|
application.run_polling(allowed_updates=Update.ALL_TYPES)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
if not TELEGRAM_BOT_TOKEN:
|
|
||||||
logger.error("TELEGRAM_BOT_TOKEN is not set. Please set the environment variable.")
|
|
||||||
exit(1)
|
|
||||||
if not all([QBT_HOST, QBT_USERNAME, QBT_PASSWORD]):
|
|
||||||
logger.warning("QBittorrent connection details (QBT_HOST, QBT_USERNAME, QBT_PASSWORD) are not fully set. Bot will attempt to connect on first use of qBittorrent related commands.")
|
|
||||||
main()
|
main()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user