Initial commit
This commit is contained in:
+10
@@ -0,0 +1,10 @@
|
||||
FROM python:3.12-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt .
|
||||
COPY main.py .
|
||||
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
ENTRYPOINT ["python", "main.py"]
|
||||
@@ -0,0 +1,337 @@
|
||||
import qbittorrentapi
|
||||
import qbittorrentapi.exceptions as qexceptions
|
||||
import subprocess
|
||||
import humanize
|
||||
import requests
|
||||
import asyncio
|
||||
import aiohttp
|
||||
import enum
|
||||
from telebot.async_telebot import AsyncTeleBot
|
||||
from telebot.formatting import escape_markdown
|
||||
import jellyfin_api_client as jellyfin
|
||||
import telebot.types as types
|
||||
|
||||
class States(enum.Enum):
|
||||
AWAITING_FOR_TORRENT = 0
|
||||
AWAITING_FOR_CATEGORY = 1
|
||||
AWAITING_FOR_TORRENT_NUMBER = 2
|
||||
NONE = 3
|
||||
|
||||
conn_info = dict(
|
||||
host="qbittorrent",
|
||||
port=8080,
|
||||
username="family",
|
||||
password="123456qBittorrent",
|
||||
)
|
||||
qbt_client = qbittorrentapi.Client(**conn_info)
|
||||
qbt_client.auth_log_in()
|
||||
print(f"qBittorrent: {qbt_client.app.version}")
|
||||
print(f"qBittorrent Web API: {qbt_client.app.web_api_version}")
|
||||
for k, v in qbt_client.app.build_info.items():
|
||||
print(f"{k}: {v}")
|
||||
|
||||
bot = AsyncTeleBot('8259146079:AAHpPFoDYK5nw3idJlJvSx6pck8xP5vQG8M')
|
||||
|
||||
status_cache = {}
|
||||
awaiting_torrents = {}
|
||||
|
||||
main_markup = types.ReplyKeyboardMarkup(row_width=2, resize_keyboard=True, one_time_keyboard=True)
|
||||
main_markup.row(types.KeyboardButton("qBittorrent"), types.KeyboardButton("Jellyfin"))
|
||||
main_markup.add(types.KeyboardButton("Management"))
|
||||
|
||||
def format_seconds(seconds):
|
||||
if seconds < 60:
|
||||
return f"{seconds}s"
|
||||
elif seconds < 3600:
|
||||
return f"{seconds // 60}m {seconds % 60}s"
|
||||
elif seconds < 86400:
|
||||
hours = seconds // 3600
|
||||
minutes = (seconds % 3600) // 60
|
||||
return f"{hours}h {minutes}m"
|
||||
else:
|
||||
days = seconds // 86400
|
||||
hours = (seconds % 86400) // 3600
|
||||
return f"{days}d {hours}h"
|
||||
|
||||
def human_speed(speed_bytes):
|
||||
if speed_bytes < 1024:
|
||||
return f"{speed_bytes} B/s"
|
||||
return humanize.naturalsize(speed_bytes, binary=True, format='%.1f') + "/s"
|
||||
|
||||
def ping_host(ip: str, count: int = 2) -> bool:
|
||||
cmd = ["ping", "-c", str(count), ip] # use "-n" instead of "-c" on Windows
|
||||
try:
|
||||
subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
return True
|
||||
except subprocess.CalledProcessError:
|
||||
return False
|
||||
|
||||
@bot.message_handler(commands=['help', 'start'])
|
||||
async def send_welcome(message):
|
||||
await bot.send_message(
|
||||
message.chat.id,
|
||||
"Hello! Choose an option below:",
|
||||
reply_markup=main_markup
|
||||
)
|
||||
|
||||
@bot.message_handler(func=lambda message: message.text == "Go back")
|
||||
async def go_back(message):
|
||||
await send_welcome(message)
|
||||
|
||||
@bot.message_handler(func=lambda message: message.text == "qBittorrent")
|
||||
async def update_menus(message):
|
||||
markup = types.ReplyKeyboardMarkup(row_width=2, resize_keyboard=True, one_time_keyboard=True)
|
||||
markup.row(types.KeyboardButton("Add torrent"), types.KeyboardButton("Delete torrent"))
|
||||
markup.add(types.KeyboardButton("Get list of active torrents"))
|
||||
markup.row(types.KeyboardButton("Get total stats"), types.KeyboardButton("Go back"))
|
||||
await bot.send_message(
|
||||
message.chat.id,
|
||||
"Select action.",
|
||||
reply_markup=markup
|
||||
)
|
||||
|
||||
@bot.message_handler(func=lambda message: message.text == "Jellyfin")
|
||||
async def update_jf_menus(message):
|
||||
markup = types.ReplyKeyboardMarkup(row_width=2, resize_keyboard=True, one_time_keyboard=True)
|
||||
markup.add(types.KeyboardButton("Refresh Jellyfin"))
|
||||
await bot.send_message(
|
||||
message.chat.id,
|
||||
"Select action.",
|
||||
reply_markup=markup
|
||||
)
|
||||
|
||||
@bot.message_handler(func=lambda message: message.text == "Management")
|
||||
async def update_mgmt_menus(message):
|
||||
markup = types.ReplyKeyboardMarkup(row_width=2, resize_keyboard=True, one_time_keyboard=True)
|
||||
markup.add(types.KeyboardButton("Ping"))
|
||||
await bot.send_message(
|
||||
message.chat.id,
|
||||
"Select action.",
|
||||
reply_markup=markup
|
||||
)
|
||||
|
||||
@bot.message_handler(func=lambda message: message.text == "Ping")
|
||||
async def check_reachability(message):
|
||||
await bot.send_message(
|
||||
message.chat.id,
|
||||
"Testing..."
|
||||
)
|
||||
response = "<b> Public endpoint reachability </b>\n"
|
||||
try:
|
||||
resp = requests.get("https://headscale.hyperwin-homeserver.duckdns.org/health", timeout=1)
|
||||
if resp.status_code == 200:
|
||||
response += "🟢 Headscale endpoint\n"
|
||||
except:
|
||||
response += "🔴 Headscale endpoint\n"
|
||||
|
||||
try:
|
||||
resp = requests.get("https://immich.hyperwin-homeserver.duckdns.org", timeout=1)
|
||||
if resp.status_code == 200:
|
||||
response += "🟢 Immich endpoint\n"
|
||||
except:
|
||||
response += "🔴 Immich endpoint\n"
|
||||
|
||||
if (ping_host("192.168.0.235")):
|
||||
response += "🟢 Raspberry Pi 4\n"
|
||||
else:
|
||||
response += "🔴 Raspberry Pi 4\n"
|
||||
|
||||
if (ping_host("45.81.35.5")):
|
||||
response += "🟢 VPS\n"
|
||||
else:
|
||||
response += "🔴 VPS\n"
|
||||
|
||||
await bot.send_message(
|
||||
message.chat.id,
|
||||
response,
|
||||
parse_mode='HTML',
|
||||
reply_markup=main_markup
|
||||
)
|
||||
|
||||
|
||||
@bot.message_handler(func=lambda message: message.text == "Add torrent")
|
||||
async def handle_torrent(message):
|
||||
status_cache[message.chat.id] = States.AWAITING_FOR_TORRENT
|
||||
await bot.reply_to(message, "Please send a magnet link or a torrent file")
|
||||
|
||||
@bot.message_handler(func=lambda message: message.text == "Get list of active torrents")
|
||||
async def get_active_torrents(message):
|
||||
torrents = qbt_client.torrents_info(status_filter="downloading")
|
||||
torrents.sort(key=lambda x: x.name.lower())
|
||||
msg = ""
|
||||
for i, torrent in enumerate(torrents, start=1):
|
||||
msg += f"- {torrent.name}, ETA: {format_seconds(int(torrent.eta))}"
|
||||
if (msg == ""):
|
||||
msg = "No torrents are being downloaded at the moment."
|
||||
await bot.send_message(
|
||||
message.chat.id,
|
||||
msg,
|
||||
reply_markup=main_markup
|
||||
)
|
||||
|
||||
@bot.message_handler(func=lambda message: message.text == "Get total stats")
|
||||
async def send_qb_stats(message):
|
||||
try:
|
||||
# Get server state with all metrics
|
||||
maindata = qbt_client.sync_maindata()
|
||||
server_state = maindata.get('server_state', {})
|
||||
transfer = qbt_client.transfer_info()
|
||||
|
||||
# Extract metrics with fallbacks
|
||||
session_dl = humanize.naturalsize(transfer.dl_info_data, binary=True)
|
||||
session_ul = humanize.naturalsize(transfer.up_info_data, binary=True)
|
||||
current_dl = human_speed(transfer.dl_info_speed)
|
||||
current_ul = human_speed(transfer.up_info_speed)
|
||||
|
||||
# Get torrent states
|
||||
torrents = qbt_client.torrents_info()
|
||||
states = [t.state for t in torrents]
|
||||
downloading = sum(1 for s in states if 'downloading' in s and 'paused' not in s)
|
||||
paused = sum(1 for s in states if 'paused' in s)
|
||||
total = len(torrents)
|
||||
|
||||
# Format metrics with clear sections
|
||||
stats = (
|
||||
"📊 <b>qBittorrent Metrics</b>\n\n"
|
||||
|
||||
"🔄 <b>Current Session</b>\n"
|
||||
f"⬇️ Downloaded: {session_dl}\n"
|
||||
f"⬆️ Uploaded: {session_ul}\n\n"
|
||||
|
||||
"⚡ <b>Live Speeds</b>\n"
|
||||
f"⏬ Down: {current_dl}\n"
|
||||
f"⏫ Up: {current_ul}\n\n"
|
||||
|
||||
"🧩 <b>Torrent Summary</b>\n"
|
||||
f"✅ Total: {total}\n"
|
||||
f"⏬ Active DL: {downloading}\n"
|
||||
f"⏸️ Paused: {paused}"
|
||||
)
|
||||
|
||||
await bot.send_message(
|
||||
message.chat.id,
|
||||
stats,
|
||||
parse_mode='HTML',
|
||||
reply_markup=main_markup
|
||||
)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
error_msg = (
|
||||
"❌ <b>Error fetching metrics:</b>\n"
|
||||
)
|
||||
bot.reply_to(message, error_msg, parse_mode='HTML', reply_markup=main_markup)
|
||||
|
||||
@bot.message_handler(func=lambda message: message.text =="Delete torrent")
|
||||
async def delete_torrent(message):
|
||||
torrents = qbt_client.torrents_info(status_filter="completed")
|
||||
torrents.sort(key=lambda x: x.name.lower())
|
||||
msg = ""
|
||||
for i, torrent in enumerate(torrents, start=1):
|
||||
msg += f"**{i}**\\. {escape_markdown(torrent.name)}\n"
|
||||
await bot.send_message(
|
||||
message.chat.id,
|
||||
msg,
|
||||
parse_mode="MarkdownV2"
|
||||
)
|
||||
status_cache[message.chat.id] = States.AWAITING_FOR_TORRENT_NUMBER
|
||||
|
||||
@bot.message_handler(func=lambda message: message.text == "Refresh Jellyfin")
|
||||
async def jellyfin_rescan_all(message):
|
||||
url = "http://jellyfin:8096/Library/Refresh"
|
||||
headers = {"X-Emby-Token": "d2ca7e1161d04b1aab5118d1f5982962"}
|
||||
params = {"ReplaceAllMetadata": "true", "ScanLibrary": "true"}
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(url, headers=headers, params=params) as response:
|
||||
if response.content_length:
|
||||
content = await response.read()
|
||||
await bot.send_message(message.chat.id, content.decode())
|
||||
else:
|
||||
await bot.send_message(
|
||||
message.chat.id,
|
||||
"Library refresh started. Please wait a few minutes and then restart your client.",
|
||||
reply_markup=main_markup
|
||||
)
|
||||
|
||||
@bot.message_handler(content_types=['text'])
|
||||
async def handle_message(message):
|
||||
if (status_cache[message.chat.id] == States.AWAITING_FOR_TORRENT):
|
||||
markup = types.ReplyKeyboardMarkup(row_width=3, resize_keyboard=True, one_time_keyboard=True)
|
||||
for category in qbt_client.torrents_categories().keys():
|
||||
markup.add(category)
|
||||
awaiting_torrents[message.chat.id] = message.text.strip()
|
||||
status_cache[message.chat.id] = States.AWAITING_FOR_CATEGORY
|
||||
await bot.send_message(
|
||||
message.chat.id,
|
||||
"Select torrent category.",
|
||||
reply_markup=markup
|
||||
)
|
||||
elif (status_cache[message.chat.id] == States.AWAITING_FOR_CATEGORY):
|
||||
category = message.text.strip()
|
||||
if (category not in qbt_client.torrents_categories().keys()):
|
||||
markup = types.ReplyKeyboardMarkup(row_width=3, resize_keyboard=True, one_time_keyboard=True)
|
||||
for category in qbt_client.torrents_categories().keys():
|
||||
markup.add(category)
|
||||
await bot.send_message(
|
||||
message.chat.id,
|
||||
"Select torrent category.",
|
||||
reply_markup=markup
|
||||
)
|
||||
return
|
||||
try:
|
||||
print(awaiting_torrents[message.chat.id])
|
||||
print(category)
|
||||
result = qbt_client.torrents_add(urls=awaiting_torrents[message.chat.id], category=category)
|
||||
if (result == "Ok."):
|
||||
await bot.send_message(
|
||||
message.chat.id,
|
||||
"Torrent added successfully! Track download progress in qBitController",
|
||||
reply_markup=main_markup
|
||||
)
|
||||
else:
|
||||
await bot.send_message(
|
||||
message.chat.id,
|
||||
f"Failed to add torrent. Error: '{result}'",
|
||||
reply_markup=main_markup
|
||||
)
|
||||
except qexceptions.Conflict409Error as err:
|
||||
await bot.send_message(
|
||||
message.chat.id,
|
||||
"Torrent already has been added"
|
||||
)
|
||||
print(err)
|
||||
elif (status_cache[message.chat.id] == States.AWAITING_FOR_TORRENT_NUMBER):
|
||||
number = 0
|
||||
try:
|
||||
number = int(message.text)
|
||||
except:
|
||||
await bot.send_message(message.chat.id, "Please, send a number")
|
||||
torrents = qbt_client.torrents_info(status_filter="completed")
|
||||
torrents.sort(key=lambda x: x.name.lower())
|
||||
try:
|
||||
qbt_client.torrents_delete(delete_files=True, torrent_hashes=torrents[number - 1].hash.lower())
|
||||
except Exception as err:
|
||||
print(err)
|
||||
if qbt_client.torrents_info(torrent_hashes=torrents[number - 1].hash.lower()):
|
||||
raise Exception(f"Torrent still exists after deletion attempt! Hash: {torrents[number - 1].hash.lower()}")
|
||||
if (qbt_client.torrents_info(torrent_hashes=torrents[number - 1].hash.lower())):
|
||||
await bot.send_message(
|
||||
message.chat.id,
|
||||
"Failed to delete the torrent!",
|
||||
reply_markup=main_markup
|
||||
)
|
||||
else:
|
||||
await bot.send_message(
|
||||
message.chat.id,
|
||||
"Torrent deleted successfully.",
|
||||
reply_markup=main_markup
|
||||
)
|
||||
status_cache[message.chat.id] == States.NONE
|
||||
|
||||
while True:
|
||||
try:
|
||||
asyncio.run(bot.infinity_polling())
|
||||
except Exception as err:
|
||||
print(err)
|
||||
continue
|
||||
break
|
||||
@@ -0,0 +1,5 @@
|
||||
telebot
|
||||
aiohttp
|
||||
qbittorrent-api
|
||||
jellyfin-api-client
|
||||
humanize
|
||||
Reference in New Issue
Block a user