mirror of
https://github.com/NohamR/TransmissionBot.git
synced 2025-05-24 14:22:00 +00:00
Create bot.py
initial commit
This commit is contained in:
parent
f7b93be8e0
commit
6e11b62fa8
813
bot.py
Normal file
813
bot.py
Normal file
@ -0,0 +1,813 @@
|
||||
""""
|
||||
Copyright © twilsonco 2020
|
||||
Description: This is a discord bot to manage torrent transfers through the Transmission transmissionrpc python library.
|
||||
|
||||
Version: 1.0
|
||||
"""
|
||||
|
||||
import discord
|
||||
import asyncio
|
||||
import aiohttp
|
||||
import json
|
||||
import subprocess
|
||||
from discord.ext.commands import Bot
|
||||
from discord.ext import commands
|
||||
from platform import python_version
|
||||
import os
|
||||
from os.path import expanduser, join
|
||||
import re
|
||||
import datetime
|
||||
import pytz
|
||||
import platform
|
||||
import transmissionrpc
|
||||
import logging
|
||||
|
||||
BOT_PREFIX = 't/'
|
||||
TOKEN = 'SECRET_TOKEN'
|
||||
OWNERS = [OWNER_USER_IDS]
|
||||
BLACKLIST = [USER_IDS]
|
||||
WHITELIST = [USER_IDS]
|
||||
CHANNEL_IDS=[CHANNEL_IDS]
|
||||
LOGO_URL="https://iyanovich.files.wordpress.com/2009/04/transmission-logo.png"
|
||||
|
||||
TSCLIENT_CONFIG={
|
||||
'host': "10.0.1.2",
|
||||
'port': 9091,
|
||||
'user': "USERNAME",
|
||||
'password': "PASSWORD"
|
||||
}
|
||||
DRYRUN = False
|
||||
|
||||
logging.basicConfig(format='%(asctime)s %(message)s',filename=join(expanduser("~"),'transmissionbot.log'))
|
||||
|
||||
|
||||
|
||||
client = Bot(command_prefix=BOT_PREFIX)
|
||||
TSCLIENT = None
|
||||
|
||||
|
||||
|
||||
logger = logging.getLogger('transmission_bot')
|
||||
logger.setLevel(logging.INFO)
|
||||
DEFAULT_REASON="TransmissionBot"
|
||||
|
||||
# Begin transmissionrpc functions, lovingly taken from https://github.com/leighmacdonald/transmission_scripts
|
||||
|
||||
class TSClient(transmissionrpc.Client):
|
||||
""" Basic subclass of the standard transmissionrpc client which provides some simple
|
||||
helper functionality.
|
||||
"""
|
||||
|
||||
def get_torrents_by(self, sort_by=None, filter_by=None, reverse=False, filter_regex=None, id_list=None):
|
||||
"""This method will call get_torrents and then perform any sorting or filtering
|
||||
actions requested on the returned torrent set.
|
||||
|
||||
:param sort_by: Sort key which must exist in `Sort.names` to be valid;
|
||||
:type sort_by: str
|
||||
:param filter_by:
|
||||
:type filter_by: str
|
||||
:param reverse:
|
||||
:return: Sorted and filter torrent list
|
||||
:rtype: transmissionrpc.Torrent[]
|
||||
"""
|
||||
torrents = self.get_torrents()
|
||||
if filter_regex:
|
||||
regex = re.compile(filter_regex, re.IGNORECASE)
|
||||
torrents = [tor for tor in torrents if regex.search(tor.name)]
|
||||
if id_list:
|
||||
torrents = [tor for tor in torrents if tor.id in id_list]
|
||||
if filter_by:
|
||||
torrents = filter_torrents_by(torrents, key=getattr(Filter, filter_by))
|
||||
if sort_by is None:
|
||||
if filter_by == "downloading":
|
||||
sort_by = "speed_down"
|
||||
elif filter_by == "seeding":
|
||||
sort_by = "speed_up"
|
||||
elif filter_by in ["stopped","finished"]:
|
||||
sort_by = "ratio"
|
||||
if sort_by:
|
||||
torrents = sort_torrents_by(torrents, key=getattr(Sort, sort_by), reverse=reverse)
|
||||
return torrents
|
||||
|
||||
def make_client():
|
||||
""" Create a new transmission RPC client
|
||||
|
||||
If you want to parse more than the standard CLI arguments, like when creating a new customized
|
||||
script, you can append your options to the argument parser.
|
||||
|
||||
:param args: Optional CLI args passed in.
|
||||
:return:
|
||||
"""
|
||||
try:
|
||||
return TSClient(
|
||||
TSCLIENT_CONFIG['host'],
|
||||
port=TSCLIENT_CONFIG['port'],
|
||||
user=TSCLIENT_CONFIG['user'],
|
||||
password=TSCLIENT_CONFIG['password']
|
||||
)
|
||||
except:
|
||||
return None
|
||||
|
||||
filter_names = (
|
||||
"all",
|
||||
"active",
|
||||
"downloading",
|
||||
"seeding",
|
||||
"stopped",
|
||||
"finished"
|
||||
)
|
||||
|
||||
class Filter(object):
|
||||
"""A set of filtering operations that can be used against a list of torrent objects"""
|
||||
|
||||
names = (
|
||||
"all",
|
||||
"active",
|
||||
"downloading",
|
||||
"seeding",
|
||||
"stopped",
|
||||
"finished"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def all(t):
|
||||
return t
|
||||
|
||||
@staticmethod
|
||||
def active(t):
|
||||
return t.rateUpload > 0 or t.rateDownload > 0
|
||||
|
||||
@staticmethod
|
||||
def downloading(t):
|
||||
return t.status == 'downloading'
|
||||
|
||||
@staticmethod
|
||||
def seeding(t):
|
||||
return t.status == 'seeding'
|
||||
|
||||
@staticmethod
|
||||
def stopped(t):
|
||||
return t.status == 'stopped'
|
||||
|
||||
@staticmethod
|
||||
def finished(t):
|
||||
return t.status == 'finished'
|
||||
|
||||
@staticmethod
|
||||
def lifetime(t):
|
||||
return t.date_added
|
||||
|
||||
|
||||
def filter_torrents_by(torrents, key=Filter.all):
|
||||
"""
|
||||
|
||||
:param key:
|
||||
:param torrents:
|
||||
:return: []transmissionrpc.Torrent
|
||||
"""
|
||||
filtered_torrents = []
|
||||
for torrent in torrents:
|
||||
if key(torrent):
|
||||
filtered_torrents.append(torrent)
|
||||
return filtered_torrents
|
||||
|
||||
sort_names = (
|
||||
"id",
|
||||
"progress",
|
||||
"name",
|
||||
"size",
|
||||
"ratio",
|
||||
"speed",
|
||||
"speed_up",
|
||||
"speed_down",
|
||||
"status",
|
||||
"queue",
|
||||
"age",
|
||||
"activity"
|
||||
)
|
||||
|
||||
class Sort(object):
|
||||
""" Defines methods for sorting torrent sequences """
|
||||
|
||||
names = (
|
||||
"id",
|
||||
"progress",
|
||||
"name",
|
||||
"size",
|
||||
"ratio",
|
||||
"speed",
|
||||
"speed_up",
|
||||
"speed_down",
|
||||
"status",
|
||||
"queue",
|
||||
"age",
|
||||
"activity"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def activity(t):
|
||||
return t.date_active
|
||||
|
||||
@staticmethod
|
||||
def age(t):
|
||||
return t.date_added
|
||||
|
||||
@staticmethod
|
||||
def queue(t):
|
||||
return t.queue_position
|
||||
|
||||
@staticmethod
|
||||
def status(t):
|
||||
return t.status
|
||||
|
||||
@staticmethod
|
||||
def progress(t):
|
||||
return t.progress
|
||||
|
||||
@staticmethod
|
||||
def name(t):
|
||||
return t.name.lower()
|
||||
|
||||
@staticmethod
|
||||
def size(t):
|
||||
return -t.totalSize
|
||||
|
||||
@staticmethod
|
||||
def id(t):
|
||||
return t.id
|
||||
|
||||
@staticmethod
|
||||
def ratio(t):
|
||||
return t.ratio
|
||||
|
||||
@staticmethod
|
||||
def speed(t):
|
||||
return t.rateUpload + t.rateDownload
|
||||
|
||||
@staticmethod
|
||||
def speed_up(t):
|
||||
return t.rateUpload
|
||||
|
||||
@staticmethod
|
||||
def speed_down(t):
|
||||
return t.rateDownload
|
||||
|
||||
|
||||
def sort_torrents_by(torrents, key=Sort.name, reverse=False):
|
||||
return sorted(torrents, key=key, reverse=reverse)
|
||||
|
||||
# def print_torrent_line(torrent, colourize=True):
|
||||
# name = torrent.name
|
||||
# progress = torrent.progress / 100.0
|
||||
# print("[{}] [{}] {} {}[{}/{}]{} ra: {} up: {} dn: {} [{}]".format(
|
||||
# white_on_blk(torrent.id),
|
||||
# find_tracker(torrent),
|
||||
# print_pct(torrent) if colourize else name.decode("latin-1"),
|
||||
# white_on_blk(""),
|
||||
# red_on_blk("{:.0%}".format(progress)) if progress < 1 else green_on_blk("{:.0%}".format(progress)),
|
||||
# magenta_on_blk(natural_size(torrent.totalSize)),
|
||||
# white_on_blk(""),
|
||||
# red_on_blk(torrent.ratio) if torrent.ratio < 1.0 else green_on_blk(torrent.ratio),
|
||||
# green_on_blk(natural_size(float(torrent.rateUpload)) + "/s") if torrent.rateUpload else "0.0 kB/s",
|
||||
# green_on_blk(natural_size(float(torrent.rateDownload)) + "/s") if torrent.rateDownload else "0.0 kB/s",
|
||||
# yellow_on_blk(torrent.status)
|
||||
# ))
|
||||
|
||||
def remove_torrent(torrent, reason=DEFAULT_REASON, delete_files=False):
|
||||
""" Remove a torrent from the client stopping it first if its in a started state.
|
||||
|
||||
:param client: Transmission RPC Client
|
||||
:type client: transmissionrpc.Client
|
||||
:param torrent: Torrent instance to remove
|
||||
:type torrent: transmissionrpc.Torrent
|
||||
:param reason: Reason for removal
|
||||
:type reason: str
|
||||
:param dry_run: Do a dry run without actually running any commands
|
||||
:type dry_run: bool
|
||||
:return:
|
||||
"""
|
||||
if torrent.status != "stopped":
|
||||
if not DRYRUN:
|
||||
TSCLIENT.stop_torrent(torrent.hashString)
|
||||
if not DRYRUN:
|
||||
TSCLIENT.remove_torrent(torrent.hashString, delete_data=delete_files)
|
||||
logger.info("Removed: {} {}\n\tReason: {}\n\tDry run: {}, Delete files: {}".format(torrent.name, torrent.hashString, reason, DRYRUN,delete_files))
|
||||
|
||||
def remove_torrents(torrents, reason=DEFAULT_REASON, delete_files=False):
|
||||
""" Remove a torrent from the client stopping it first if its in a started state.
|
||||
|
||||
:param client: Transmission RPC Client
|
||||
:type client: transmissionrpc.Client
|
||||
:param torrent: Torrent instance to remove
|
||||
:type torrent: transmissionrpc.Torrent
|
||||
:param reason: Reason for removal
|
||||
:type reason: str
|
||||
:param dry_run: Do a dry run without actually running any commands
|
||||
:type dry_run: bool
|
||||
:return:
|
||||
"""
|
||||
for torrent in torrents:
|
||||
remove_torrent(torrent, reason=reason, delete_files=delete_files)
|
||||
|
||||
def stop_torrents(torrents=[], reason=DEFAULT_REASON):
|
||||
""" Stop (pause) a list of torrents from the client.
|
||||
|
||||
:param client: Transmission RPC Client
|
||||
:type client: transmissionrpc.Client
|
||||
:param torrent: Torrent instance to remove
|
||||
:type torrent: transmissionrpc.Torrent
|
||||
:param reason: Reason for removal
|
||||
:type reason: str
|
||||
:param dry_run: Do a dry run without actually running any commands
|
||||
:type dry_run: bool
|
||||
:return:
|
||||
"""
|
||||
for torrent in (torrents if len(torrents) > 0 else TSCLIENT.get_torrents()):
|
||||
if torrent.status not in ["stopped","finished"]:
|
||||
if not DRYRUN:
|
||||
TSCLIENT.stop_torrent(torrent.hashString)
|
||||
logger.info("Paused: {} {}\n\tReason: {}\n\tDry run: {}".format(torrent.name, torrent.hashString, reason, DRYRUN))
|
||||
|
||||
def resume_torrents(torrents=[], reason=DEFAULT_REASON):
|
||||
""" Stop (pause) a list of torrents from the client.
|
||||
|
||||
:param client: Transmission RPC Client
|
||||
:type client: transmissionrpc.Client
|
||||
:param torrent: Torrent instance to remove
|
||||
:type torrent: transmissionrpc.Torrent
|
||||
:param reason: Reason for removal
|
||||
:type reason: str
|
||||
:param dry_run: Do a dry run without actually running any commands
|
||||
:type dry_run: bool
|
||||
:return:
|
||||
"""
|
||||
for torrent in (torrents if len(torrents) > 0 else TSCLIENT.get_torrents()):
|
||||
if torrent.status == "stopped":
|
||||
if not DRYRUN:
|
||||
TSCLIENT.start_torrent(torrent.hashString)
|
||||
logger.info("Resumed: {} {}\n\tReason: {}\n\tDry run: {}".format(torrent.name, torrent.hashString, reason, DRYRUN))
|
||||
|
||||
def add_torrent(torStr):
|
||||
tor = None
|
||||
if torStr != "":
|
||||
tor = TSCLIENT.add_torrent(torStr)
|
||||
return tor
|
||||
|
||||
|
||||
# Begin discord bot functions, adapted from https://github.com/kkrypt0nn/Python-Discord-Bot-Template
|
||||
|
||||
# async def status_task():
|
||||
# while True:
|
||||
# await client.change_presence(activity=discord.Game("{}help".format(BOT_PREFIX)))
|
||||
# await asyncio.sleep(86400)
|
||||
|
||||
@client.event
|
||||
async def on_ready():
|
||||
global TSCLIENT
|
||||
TSCLIENT = make_client()
|
||||
if TSCLIENT is None:
|
||||
print("Failed to create transmissionrpc client")
|
||||
else:
|
||||
# client.loop.create_task(status_task())
|
||||
await client.change_presence(activity=discord.Game("{}help".format(BOT_PREFIX)))
|
||||
print('Logged in as ' + client.user.name)
|
||||
print("Discord.py API version:", discord.__version__)
|
||||
print("Python version:", platform.python_version())
|
||||
print("Running on:", platform.system(), platform.release(), "(" + os.name + ")")
|
||||
print('-------------------')
|
||||
|
||||
def humanbytes(B):
|
||||
'Return the given bytes as a human friendly KB, MB, GB, or TB string'
|
||||
B = float(B)
|
||||
KB = float(1024)
|
||||
MB = float(KB ** 2) # 1,048,576
|
||||
GB = float(KB ** 3) # 1,073,741,824
|
||||
TB = float(KB ** 4) # 1,099,511,627,776
|
||||
|
||||
if B < KB:
|
||||
return '{0} {1}'.format(B,'B')
|
||||
elif KB <= B < MB:
|
||||
return '{0:.2f} kB'.format(B/KB)
|
||||
elif MB <= B < GB:
|
||||
return '{0:.2f} MB'.format(B/MB)
|
||||
elif GB <= B < TB:
|
||||
return '{0:.2f} GB'.format(B/GB)
|
||||
elif TB <= B:
|
||||
return '{0:.2f} TB'.format(B/TB)
|
||||
|
||||
def tobytes(B):
|
||||
'Return the number of bytes given by a string (a float followed by a space and the unit of prefix-bytes eg. "21.34 GB")'
|
||||
numstr = B.lower().split(' ')
|
||||
KB = (('kilo','kb','kb/s'),float(1024))
|
||||
MB = (('mega','mb','mb/s'),float(KB[1] ** 2)) # 1,048,576
|
||||
GB = (('giga','gb','gb/s'),float(KB[1] ** 3)) # 1,073,741,824
|
||||
TB = (('tera','tb','tb/s'),float(KB[1] ** 4)) # 1,099,511,627,776
|
||||
|
||||
for prefix in (KB,MB,GB,TB):
|
||||
if numstr[1] in prefix[0]:
|
||||
return float(float(numstr[0]) * prefix[1])
|
||||
|
||||
# check that message author is allowed and message was sent in allowed channel
|
||||
async def CommandPrecheck(context):
|
||||
if len(CHANNEL_IDS) > 0 and context.message.channel.id not in CHANNEL_IDS:
|
||||
await context.message.channel.send("I don't respond to commands in this channel...")
|
||||
return False
|
||||
elif context.message.author.id in BLACKLIST or (len(WHITELIST) > 0 and context.message.author.id not in WHITELIST):
|
||||
await context.message.channel.send("You're not allowed to use this...")
|
||||
return False
|
||||
return True
|
||||
|
||||
@client.command(name='add', pass_context=True)
|
||||
async def add(context, *, content):
|
||||
if await CommandPrecheck(context):
|
||||
if content == "":
|
||||
await context.message.channel.send("Invalid string")
|
||||
else:
|
||||
torStr = None
|
||||
for t in content.strip().split(" "):
|
||||
await context.message.channel.send('Adding torrent {}\n Please wait...'.format(t))
|
||||
tor = add_torrent(t)
|
||||
if torStr:
|
||||
torStr += "\n{}" % tor.name
|
||||
else:
|
||||
torStr = tor.name
|
||||
await context.message.channel.send('✅ Added torrent{}:\n{}'.format("s" if len(content.strip().split(" ")) > 1 else "", torStr))
|
||||
@client.command(name='a', pass_context=True)
|
||||
async def a(context, *, content=""):
|
||||
await add(context, content=content)
|
||||
|
||||
# def torInfo(t):
|
||||
# states = ('downloading', 'seeding', 'stopped', 'finished','all')
|
||||
# stateEmoji = {i:j for i,j in zip(states,['🔻','🌱','⏸','🏁','🔁'])}
|
||||
#
|
||||
# downStr = humanbytes(t.progress * 0.01 * t.totalSize)
|
||||
# upStr = "{} (Ratio: {:.2f})".format(humanbytes(t.uploadedEver), t.uploadRatio)
|
||||
# runTime =
|
||||
#
|
||||
# if t.progress < 100.0:
|
||||
# have = "{} of {} ({:.1f}){}{}".format(downStr,humanbytes(t.totalSize), t.progress, '' if t.haveUnchecked == 0 else ', {} Unverified'.format(humanbytes(t.haveUnchecked)), '' if t.corruptEver == 0 else ', {} Corrupt'.format(humanbytes(t.corruptEver)))
|
||||
# avail = "{:.1f}%".format(t.desiredAvailable/t.leftUntilDone)
|
||||
# else:
|
||||
# have = "{} ({:d}){}{}".format(humanbytes(t.totalSize), t.progress, '' if t.haveUnchecked == 0 else ', {} Unverified'.format(humanbytes(t.haveUnchecked)), '' if t.corruptEver == 0 else ', {} Corrupt'.format(humanbytes(t.corruptEver)))
|
||||
# avail = "100%"
|
||||
#
|
||||
#
|
||||
#
|
||||
# embed=discord.Embed(title=t.name,color=0xb51a00)
|
||||
#
|
||||
# return embed
|
||||
|
||||
torListLegend="• Legend:\n⬇️download rate, ⬆️upload rate, ⚖️seed ratio, 🔻downloading, 🌱seeding, 🏁finished, ⏸paused\n• Error:\n✅none, ⚠️tracker warning, ☄️tracker error, 🔥local error\n• Info:\n🐢stalled, 🐇running, 🔒private, 🔓public "
|
||||
|
||||
def torList(torrents, author_name="Torrent Transfers",title=None,description=None):
|
||||
states = ('downloading', 'seeding', 'stopped', 'finished','all')
|
||||
stateEmoji = {i:j for i,j in zip(states,['🔻','🌱','⏸','🏁','🔁'])}
|
||||
errorStrs = ['✅','📡⚠️','📡‼️','🖥‼️']
|
||||
|
||||
def torListLine(t):
|
||||
down = humanbytes(t.progress * 0.01 * t.totalSize)
|
||||
out = "{} {} {} {} ".format(stateEmoji[t.status],errorStrs[t.error],'🐢' if t.isStalled else '🐇', '🔒' if t.isPrivate else '🔓')
|
||||
if t.status == 'downloading':
|
||||
out += "{}/{} ⬇️ {}/s ⬆️ {}/s ⚖️ {:.2f}".format(down,humanbytes(t.totalSize),humanbytes(t.rateDownload),humanbytes(t.rateUpload),t.uploadRatio)
|
||||
elif t.status == 'seeding':
|
||||
out += "{} ⬆️ {}/s ⚖️ {:.2f}".format(humanbytes(t.totalSize),humanbytes(t.rateUpload),t.uploadRatio)
|
||||
elif t.status == 'stopped':
|
||||
out += "{}/{} ⚖️ {:.2f}".format(down,humanbytes(t.totalSize),t.uploadRatio)
|
||||
elif t.status == 'finished':
|
||||
out += "{} ⚖️ {:.2f}".format(humanbytes(t.totalSize),t.uploadRatio)
|
||||
|
||||
return out
|
||||
|
||||
nameList = ["{}) {:.245}{}".format(t.id,t.name,"..." if len(t.name) > 245 else "") for t in torrents]
|
||||
valList = [torListLine(t) for t in torrents]
|
||||
|
||||
n = 0
|
||||
i = 0
|
||||
embeds = []
|
||||
if len(torrents) > 0:
|
||||
while i < len(torrents):
|
||||
embed=discord.Embed(title=title,description=description,color=0xb51a00)
|
||||
embed.set_author(name=author_name, icon_url=LOGO_URL)
|
||||
for j in range(25):
|
||||
embed.add_field(name=nameList[i],value=valList[i],inline=False)
|
||||
i += 1
|
||||
n += 1
|
||||
if n >= 25:
|
||||
n = 0
|
||||
break
|
||||
if i >= len(torrents):
|
||||
break
|
||||
embed.timestamp = datetime.datetime.now(tz=pytz.timezone('America/Denver'))
|
||||
embeds.append(embed)
|
||||
|
||||
embeds[-1].set_footer(text=torListLegend)
|
||||
else:
|
||||
embeds.append(discord.Embed(title=title, description="No matching transfers found!", color=0xb51a00))
|
||||
embeds[-1].set_author(name=author_name, icon_url=LOGO_URL)
|
||||
embeds[-1].timestamp = datetime.datetime.now(tz=pytz.timezone('America/Denver'))
|
||||
|
||||
return embeds
|
||||
|
||||
def torGetListOpsFromStr(listOpStr):
|
||||
filter_by = None
|
||||
sort_by = None
|
||||
splitcontent = listOpStr.split(" ")
|
||||
|
||||
if "--filter" in splitcontent:
|
||||
ind = splitcontent.index("--filter")
|
||||
if len(splitcontent) > ind + 1:
|
||||
filter_by = splitcontent[ind+1]
|
||||
del splitcontent[ind+1]
|
||||
del splitcontent[ind]
|
||||
elif "-f" in splitcontent:
|
||||
ind = splitcontent.index("-f")
|
||||
if len(splitcontent) > ind + 1:
|
||||
filter_by = splitcontent[ind+1]
|
||||
del splitcontent[ind+1]
|
||||
del splitcontent[ind]
|
||||
|
||||
if "--sort" in splitcontent:
|
||||
ind = splitcontent.index("--sort")
|
||||
if len(splitcontent) > ind + 1:
|
||||
sort_by = splitcontent[ind+1]
|
||||
del splitcontent[ind+1]
|
||||
del splitcontent[ind]
|
||||
elif "-s" in splitcontent:
|
||||
ind = splitcontent.index("-s")
|
||||
if len(splitcontent) > ind + 1:
|
||||
sort_by = splitcontent[ind+1]
|
||||
del splitcontent[ind+1]
|
||||
del splitcontent[ind]
|
||||
|
||||
filter_regex = " ".join(splitcontent).strip()
|
||||
if filter_regex == "":
|
||||
filter_regex = None
|
||||
|
||||
if filter_by is not None and filter_by not in filter_names:
|
||||
return -1, None, None
|
||||
if sort_by is not None and sort_by not in sort_names:
|
||||
return None, -1, None
|
||||
|
||||
return filter_by, sort_by, filter_regex
|
||||
|
||||
@client.command(name='list', pass_context=True)
|
||||
async def list(context, *, content=""):
|
||||
if await CommandPrecheck(context):
|
||||
filter_by, sort_by, filter_regex = torGetListOpsFromStr(content)
|
||||
if filter_by == -1:
|
||||
await context.message.channel.send("Invalid filter specified. Choose one of {}".format(str(filter_names)))
|
||||
return
|
||||
if sort_by == -1:
|
||||
await context.message.channel.send("Invalid sort specified. Choose one of {}".format(str(sort_names)))
|
||||
return
|
||||
|
||||
torrents = TSCLIENT.get_torrents_by(sort_by=sort_by, filter_by=filter_by, filter_regex=filter_regex)
|
||||
|
||||
embeds = torList(torrents, title="{} transfer{} matching '`{}`'".format(len(torrents),'' if len(torrents)==1 else 's',content))
|
||||
|
||||
for e in embeds:
|
||||
await context.message.channel.send(embed=e)
|
||||
@client.command(name='l', pass_context=True)
|
||||
async def l(context, *, content=""):
|
||||
await list(context, content=content)
|
||||
|
||||
def torSummary(torrents):
|
||||
states = ('downloading', 'seeding', 'paused', 'finished')
|
||||
numInState = [len([True for t in torrents if t.status == s]) for s in states]
|
||||
numTot = len(torrents)
|
||||
|
||||
sumTot = sum([t.totalSize for t in torrents])
|
||||
totSize = humanbytes(sumTot)
|
||||
totUpRate = humanbytes(sum([t.rateUpload for t in torrents]))
|
||||
totDownRate = humanbytes(sum([t.rateDownload for t in torrents]))
|
||||
|
||||
downList = [t.progress*0.01*t.totalSize for t in torrents]
|
||||
upList = [t.ratio * j for t,j in zip(torrents,downList)]
|
||||
|
||||
sumDown = sum(downList)
|
||||
sumUp = sum(upList)
|
||||
|
||||
totDown = humanbytes(sumDown)
|
||||
totUp = humanbytes(sumUp)
|
||||
|
||||
totRatio = '{:.2f}'.format(sumUp / sumDown)
|
||||
|
||||
totDownRatio = '{:.2f}'.format(sumDown / sumTot * 100.0)
|
||||
|
||||
numTopRatios = min([len(torrents),5])
|
||||
topRatios = "• Top {} ratio{}:".format(numTopRatios,"s" if numTopRatios > 0 else "")
|
||||
sortByRatio = sorted(torrents,key=lambda t:float(t.ratio),reverse=True)
|
||||
for i in range(numTopRatios):
|
||||
topRatios += "\n {:.1f} {:.35}{}".format(float(sortByRatio[i].ratio),sortByRatio[i].name,"..." if len(sortByRatio[i].name) > 35 else "")
|
||||
|
||||
embed=discord.Embed(title="🌊 Torrent Summary", description="*React to see list of corresponding transfers*", color=0xb51a00)
|
||||
embed.set_thumbnail(url=LOGO_URL)
|
||||
embed.add_field(name="⬇️ {}/s".format(totDownRate), value="⬆️ {}/s".format(totUpRate), inline=False)
|
||||
embed.add_field(name="⏬ {} of {}".format(totDown,totSize), value="⏫ {} ⚖️ {}".format(totUp,totRatio), inline=False)
|
||||
embed.add_field(name="🔁 {} transfer{} total".format(numTot,"" if numTot == 0 else "s"), value="🔻 {} 🌱 {} 🏁 {} ⏸ {}".format(numInState[0],numInState[1],numInState[3],numInState[2]), inline=False)
|
||||
embed.set_footer(text=topRatios+'\n• Legend:\n⬇️total download rate, ⬆️total upload rate, ⏬total downloaded, ⏫total uploaded, 🔻downloading, 🌱seeding, 🏁finished, ⏸paused, ⚖️total seed ratio')
|
||||
embed.timestamp = datetime.datetime.now(tz=pytz.timezone('America/Denver'))
|
||||
# await context.message.channel.send(embed=embed)
|
||||
return embed,numInState
|
||||
|
||||
@client.command(name='summary', pass_context=True)
|
||||
async def summary(context, *, content=""):
|
||||
if await CommandPrecheck(context):
|
||||
states = ('downloading', 'seeding', 'paused', 'finished','all')
|
||||
stateEmoji = ['🔻','🌱','⏸','🏁','🔃']
|
||||
|
||||
summary=torSummary(TSCLIENT.get_torrents())
|
||||
msg = await context.message.channel.send(embed=summary[0])
|
||||
for i in range(len(summary[1])):
|
||||
if summary[1][i] > 0:
|
||||
await msg.add_reaction(stateEmoji[i])
|
||||
await msg.add_reaction(stateEmoji[-1])
|
||||
|
||||
def check(reaction, user):
|
||||
return user == context.message.author and str(reaction.emoji) in stateEmoji
|
||||
|
||||
try:
|
||||
reaction, user = await client.wait_for('reaction_add', timeout=60.0, check=check)
|
||||
except asyncio.TimeoutError:
|
||||
pass
|
||||
else:
|
||||
cmds = {i:j for i,j in zip(stateEmoji,('--filter downloading', '--filter seeding', '--filter stopped', '--filter finished', ''))}
|
||||
await list(context, content=cmds[str(reaction.emoji)])
|
||||
@client.command(name='s', pass_context=True)
|
||||
async def s(context, *, content=""):
|
||||
await summary(context, content=content)
|
||||
|
||||
def strListToList(strList):
|
||||
if not re.match('^[0-9\,\-]+$', strList):
|
||||
return False
|
||||
outList = []
|
||||
for seg in strList.strip().split(","):
|
||||
subseg = seg.split("-")
|
||||
if len(subseg) == 1 and int(subseg[0]) not in outList:
|
||||
outList.append(int(subseg[0]))
|
||||
elif len(subseg) == 2:
|
||||
subseg = sorted([int(i) for i in subseg])
|
||||
outList += range(subseg[0],subseg[1]+1)
|
||||
if len(outList) == 0:
|
||||
return False
|
||||
|
||||
return outList
|
||||
|
||||
@client.command(name='modify', pass_context=True)
|
||||
async def modify(context, *, content=""):
|
||||
if await CommandPrecheck(context):
|
||||
allOnly = content.strip() == ""
|
||||
torrents = []
|
||||
if not allOnly:
|
||||
id_list = strListToList(content)
|
||||
filter_by = None
|
||||
sort_by = None
|
||||
filter_regex = None
|
||||
if not id_list:
|
||||
filter_by, sort_by, filter_regex = torGetListOpsFromStr(content)
|
||||
if filter_by == -1:
|
||||
await context.message.channel.send("Invalid filter specified. Choose one of {}".format(str(filter_names)))
|
||||
return
|
||||
if sort_by == -1:
|
||||
await context.message.channel.send("Invalid sort specified. Choose one of {}".format(str(sort_names)))
|
||||
return
|
||||
# await context.message.channel.send("{} {} {} {}".format())
|
||||
# return
|
||||
torrents = TSCLIENT.get_torrents_by(filter_by=filter_by, sort_by=sort_by, filter_regex=filter_regex, id_list=id_list)
|
||||
|
||||
if len(torrents) > 0:
|
||||
ops = ["pause","resume","remove","removedelete"]
|
||||
opNames = ["pause","resume","remove","remove and delete"]
|
||||
opEmoji = ['⏸','▶️','❌','🗑']
|
||||
opStr = "⏸pause ▶️resume ❌remove 🗑remove and delete"
|
||||
embeds = torList(torrents,author_name="Click a reaction to choose modification".format(len(torrents), '' if len(torrents)==1 else 's'),title="{} transfer{} matching '`{}`' will be modified".format(len(torrents), '' if len(torrents)==1 else 's', content))
|
||||
embeds[-1].set_footer(text=torListLegend+"\n• Options:\n"+opStr)
|
||||
embeds[-1].timestamp = datetime.datetime.now(tz=pytz.timezone('America/Denver'))
|
||||
else:
|
||||
embed=discord.Embed(title="Modify transfers",color=0xb51a00)
|
||||
embed.set_author(name="No matching transfers found!", icon_url=LOGO_URL)
|
||||
embeds = [embed]
|
||||
else:
|
||||
ops = ["pauseall","resumeall"]
|
||||
opNames = ["pause all","resume all"]
|
||||
opEmoji = ['⏸','▶️']
|
||||
opStr = "⏸ pause or ▶️ resume all"
|
||||
embed=discord.Embed(title="React to choose modification",color=0xb51a00)
|
||||
embed.set_author(name="All transfers will be affected!", icon_url=LOGO_URL)
|
||||
embed.set_footer(text=opStr)
|
||||
embed.timestamp = datetime.datetime.now(tz=pytz.timezone('America/Denver'))
|
||||
embeds = [embed]
|
||||
msgs = [await context.message.channel.send(embed=e) for e in embeds]
|
||||
|
||||
if not allOnly and len(torrents) == 0:
|
||||
return
|
||||
|
||||
for i in opEmoji:
|
||||
await msgs[-1].add_reaction(i)
|
||||
|
||||
def check(reaction, user):
|
||||
return user == context.message.author and str(reaction.emoji) in opEmoji
|
||||
|
||||
try:
|
||||
reaction, user = await client.wait_for('reaction_add', timeout=60.0, check=check)
|
||||
except asyncio.TimeoutError:
|
||||
return
|
||||
else:
|
||||
cmds = {i:j for i,j in zip(opEmoji,ops)}
|
||||
cmd = cmds[str(reaction.emoji)]
|
||||
|
||||
doContinue = True
|
||||
if "remove" in cmds[str(reaction.emoji)]:
|
||||
embed=discord.Embed(title="Are you sure you wish to remove{} {} transfer{}?".format(' and DELETE' if 'delete' in cmds[str(reaction.emoji)] else '', len(torrents), '' if len(torrents)==1 else 's'),description="**This action is irreversible!**",color=0xb51a00)
|
||||
embed.set_footer(text="react ✅ to continue or ❌ to cancel")
|
||||
msg = await context.message.channel.send(embed=embed)
|
||||
|
||||
for i in ['✅','❌']:
|
||||
await msg.add_reaction(i)
|
||||
|
||||
def check1(reaction, user):
|
||||
return user == context.message.author and str(reaction.emoji) in ['✅','❌']
|
||||
try:
|
||||
reaction, user = await client.wait_for('reaction_add', timeout=60.0, check=check1)
|
||||
except asyncio.TimeoutError:
|
||||
doContinue = False
|
||||
else:
|
||||
doContinue = str(reaction.emoji) == '✅'
|
||||
if doContinue:
|
||||
if "pause" in cmd:
|
||||
stop_torrents(torrents)
|
||||
elif "resume" in cmd:
|
||||
resume_torrents(torrents)
|
||||
else:
|
||||
remove_torrents(torrents,delete_files="delete" in cmd)
|
||||
|
||||
ops = ["pause","resume","remove","removedelete","pauseall","resumeall"]
|
||||
opNames = ["paused","resumed","removed","removed and deleted","paused","resumed"]
|
||||
opEmoji = ["⏸","▶️","❌","🗑","⏸","▶️"]
|
||||
ops = {i:j for i,j in zip(ops,opNames)}
|
||||
opEmoji = {i:j for i,j in zip(ops,opEmoji)}
|
||||
await context.message.channel.send("{} Transfer{} {}".format(str(reaction.emoji),'s' if allOnly or len(torrents) > 1 else '', ops[cmd]))
|
||||
else:
|
||||
await context.message.channel.send("❌ Cancelled!")
|
||||
@client.command(name='m', pass_context=True)
|
||||
async def m(context, *, content=""):
|
||||
await modify(context, content=content)
|
||||
|
||||
client.remove_command('help')
|
||||
|
||||
@client.command(name='help', description='Help HUD.', brief='HELPOOOO!!!', pass_context=True)
|
||||
async def help(context, *, content=""):
|
||||
if await CommandPrecheck(context):
|
||||
if content != "":
|
||||
if content in ["l","list"]:
|
||||
embed = discord.Embed(title='List transfers', color=0xb51a00)
|
||||
embed.set_author(name="List current transfers with sorting, filtering, and search options", icon_url=LOGO_URL)
|
||||
embed.add_field(name="Usage", value='`{0}list [--filter FILTER] [--sort SORT] [NAME]`'.format(BOT_PREFIX), inline=False)
|
||||
embed.add_field(name="Filtering", value='`--filter FILTER` or `-f FILTER`\n`FILTER` is one of `{}`'.format(str(filter_names)), inline=False)
|
||||
embed.add_field(name="Sorting", value='`--sort SORT` or `-s SORT`\n`SORT` is one of `{}`'.format(str(sort_names)), inline=False)
|
||||
embed.add_field(name="Searching by name", value='`NAME` is a regular expression used to search transfer names (no enclosing quotes; may contain spaces)', inline=False)
|
||||
embed.add_field(name="Examples", value="*List all transfers:* `{0}list`\n*Search using phrase 'ubuntu':* `{0}l ubuntu`\n*List downloading transfers:* `{0}l -f downloading`\n*Sort transfers by age:* `{0}list --sort age`".format(BOT_PREFIX), inline=False)
|
||||
await context.message.channel.send(embed=embed)
|
||||
elif content in ["a","add"]:
|
||||
embed = discord.Embed(title='Add transfer', description="If multiple torrents are added, separate them by spaces", color=0xb51a00)
|
||||
embed.set_author(name="Add one or more specified torrents by magnet link or url to torrent file", icon_url=LOGO_URL)
|
||||
embed.add_field(name="Usage", value='`{0}add TORRENT_FILE_URL_OR_MAGNET_LINK ...`\n`{0}a TORRENT_FILE_URL_OR_MAGNET_LINK ...`'.format(BOT_PREFIX), inline=False)
|
||||
embed.add_field(name="Examples", value="*Add download of ubuntu OS:* `{0}add https://releases.ubuntu.com/20.04/ubuntu-20.04.1-desktop-amd64.iso.torrent`".format(BOT_PREFIX), inline=False)
|
||||
await context.message.channel.send(embed=embed)
|
||||
elif content in ["m","modify"]:
|
||||
embed = discord.Embed(title='Modify existing transfer(s)', color=0xb51a00)
|
||||
embed.set_author(name="Pause, resume, remove, or remove and delete specified transfer(s)", icon_url=LOGO_URL)
|
||||
embed.add_field(name="Usage", value='`{0}modify [LIST_OPTIONS] [TORRENT_ID_SPECIFIER]`'.format(BOT_PREFIX), inline=False)
|
||||
embed.add_field(name="Pause or resume ALL transfers", value="Simply run `{0}modify` to pause or resume all existing transfers".format(BOT_PREFIX), inline=False)
|
||||
embed.add_field(name="By list options", value='`LIST_OPTIONS` is a valid set of options to the `{0}list` command (see `{0}help list` for details)'.format(BOT_PREFIX), inline=False)
|
||||
embed.add_field(name="By ID specifier", value='`TORRENT_ID_SPECIFIER` is a valid transfer ID specifier—*e.g.* `1,3-5,9` to specify transfers 1, 3, 4, 5, and 9\n*Transfer IDs are the left-most number in the list of transfers (use* `{0}list` *to print full list)*'.format(BOT_PREFIX), inline=False)
|
||||
embed.add_field(name="Examples", value="`{0}modify`\n`{0}m seinfeld`\n`{0}m 23,34,36-42`\n`{0}m --filter downloading seinfeld`".format(BOT_PREFIX), inline=False)
|
||||
await context.message.channel.send(embed=embed)
|
||||
else:
|
||||
embed = discord.Embed(title='List of commands:', color=0xb51a00)
|
||||
embed.set_author(name='Transmission Bot: Manage torrent file transfers', icon_url=LOGO_URL)
|
||||
embed.add_field(name="Print summary of transfers", value="*print summary from all transfers, with followup options to list transfers*\n*ex.* `{0}summary` or `{0}s`".format(BOT_PREFIX), inline=False)
|
||||
embed.add_field(name="List torrent transfers", value="*list current transfers with sorting, filtering, and search options*\n*ex.* `{0}list [OPTIONS]` or `{0}l [OPTIONS]`".format(BOT_PREFIX), inline=False)
|
||||
embed.add_field(name="Add new torrent transfers", value="*add one or more specified torrents by magnet link or url to torrent file*\n*ex.* `{0}add TORRENT ...` or `{0}a TORRENT ...`".format(BOT_PREFIX), inline=False)
|
||||
embed.add_field(name="Modify existing transfers", value="*pause, resume, remove, or remove and delete specified transfers*\n*ex.* `{0}modify [TORRENT]` or `{0}m [TORRENT]`".format(BOT_PREFIX), inline=False)
|
||||
embed.add_field(name='Help - Gives this menu', value='*with optional details of specified command*\n*ex.* `{0}help` or `{0}help COMMAND`'.format(BOT_PREFIX), inline=False)
|
||||
|
||||
await context.message.channel.send(embed=embed)
|
||||
|
||||
@client.event
|
||||
async def on_command_error(context, error):
|
||||
if isinstance(error, commands.CommandOnCooldown):
|
||||
await context.message.delete()
|
||||
embed = discord.Embed(title="Error!", description='This command is on a {:.2f}s cooldown'.format(error.retry_after), color=0xb51a00)
|
||||
message = await context.message.channel.send(embed=embed)
|
||||
await asyncio.sleep(5)
|
||||
await message.delete()
|
||||
elif isinstance(error, commands.CommandNotFound):
|
||||
await context.message.delete()
|
||||
embed = discord.Embed(title="Error!", description="I don't know that command!", color=0xb51a00)
|
||||
message = await context.message.channel.send(embed=embed)
|
||||
await asyncio.sleep(2)
|
||||
await help(context)
|
||||
raise error
|
||||
|
||||
client.run(TOKEN)
|
Loading…
x
Reference in New Issue
Block a user