From 4d0e3d6854119edd56d78eb32c8a20d4a3a64e80 Mon Sep 17 00:00:00 2001 From: Tim Wilson Date: Tue, 25 Aug 2020 11:53:17 -0600 Subject: [PATCH] added features Repeat option for `summary` and `list` commands More filters: stalled, running (non-zero rate), error (and types of error), verifying data, queued, public/private (tracker) Added option to verify data to `modify` command Added more info to summary. --- bot.py | 547 ++++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 363 insertions(+), 184 deletions(-) diff --git a/bot.py b/bot.py index 43be241..a84b61a 100644 --- a/bot.py +++ b/bot.py @@ -18,11 +18,14 @@ import os from os.path import expanduser, join import re import datetime +import time import pytz import platform import transmissionrpc import logging +# BEGIN USER CONFIGURATION + BOT_PREFIX = 't/' TOKEN = 'SECRET_TOKEN' OWNERS = [OWNER_USER_IDS] @@ -31,31 +34,74 @@ 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 +REPEAT_FREQ = 5 # time in seconds to wait between reprinting repeated commands (in addition to the time requred to delete old message(s) and add reactions) +REPEAT_TIMEOUT = 45 # time in seconds before a repeated command automatically stops logging.basicConfig(format='%(asctime)s %(message)s',filename=join(expanduser("~"),'transmissionbot.log')) +# END USER CONFIGURATION +REPEAT_COMMAND = False +REPEAT_MSG_LIST = [] +REPEAT_START_TIME = 0 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 +filter_names = ( # these are the filters accepted by transmissionrpc + "all", + "active", + "downloading", + "seeding", + "stopped", + "finished" +) + +filter_names_extra = ( # these are extra filters I've added + "stalled", + "private", + "public", + "error", + 'err_none', + 'err_tracker_warn', + 'err_tracker_error', + 'err_local', + 'verifying', + 'queued', + "running" # running means a non-zero transfer rate, not to be confused with "active" +) + +filter_names_full = filter_names + filter_names_extra + +sort_names = ( + "id", + "progress", + "name", + "size", + "ratio", + "speed", + "speed_up", + "speed_down", + "status", + "queue", + "age", + "activity" +) + class TSClient(transmissionrpc.Client): """ Basic subclass of the standard transmissionrpc client which provides some simple helper functionality. @@ -80,13 +126,29 @@ class TSClient(transmissionrpc.Client): 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)) + for f in filter_by.split(): + if f in filter_names: + torrents = filter_torrents_by(torrents, key=getattr(Filter, filter_by)) + elif f == "verifying": + torrents = [t for t in torrents if "check" in t.status] + elif f == "queued": + torrents = [t for t in torrents if "load pending" in t.status] + elif f == "stalled": + torrents = [t for t in torrents if t.isStalled] + elif f == "private": + torrents = [t for t in torrents if t.isPrivate] + elif f == "public": + torrents = [t for t in torrents if not t.isPrivate] + elif f == "error": + torrents = [t for t in torrents if t.error != 0] + elif f == "running": + torrents = [t for t in torrents if t.rateDownload + t.rateUpload > 0] + else: + continue 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"]: + if "downloading" in filter_by or "seeding" in filter_by or "running" in filter_by: + sort_by = "speed" + elif "stopped" in filter_by or "finished" in filter_by: sort_by = "ratio" if sort_by: torrents = sort_torrents_by(torrents, key=getattr(Sort, sort_by), reverse=reverse) @@ -111,26 +173,18 @@ def make_client(): 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" - ) + # names = ( + # "all", + # "active", + # "downloading", + # "seeding", + # "stopped", + # "finished" + # ) + names = filter_names @staticmethod def all(t): @@ -174,38 +228,24 @@ def filter_torrents_by(torrents, key=Filter.all): 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" - ) + # names = ( + # "id", + # "progress", + # "name", + # "size", + # "ratio", + # "speed", + # "speed_up", + # "speed_down", + # "status", + # "queue", + # "age", + # "activity" + # ) + names = sort_names @staticmethod def activity(t): @@ -350,6 +390,23 @@ def resume_torrents(torrents=[], reason=DEFAULT_REASON): TSCLIENT.start_torrent(torrent.hashString) logger.info("Resumed: {} {}\n\tReason: {}\n\tDry run: {}".format(torrent.name, torrent.hashString, reason, DRYRUN)) +def verify_torrents(torrents=[]): + """ Verify 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 + :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 not DRYRUN: + TSCLIENT.verify_torrent(torrent.hashString) + logger.info("Verified: {} {}\n\tDry run: {}".format(torrent.name, torrent.hashString, DRYRUN)) + def add_torrent(torStr): tor = None if torStr != "": @@ -420,12 +477,13 @@ async def CommandPrecheck(context): return False return True -@client.command(name='add', pass_context=True) +@client.command(name='add', aliases=['a'], pass_context=True) async def add(context, *, content): if await CommandPrecheck(context): if content == "": await context.message.channel.send("Invalid string") else: + await context.message.delete() torStr = None for t in content.strip().split(" "): await context.message.channel.send('Adding torrent {}\n Please wait...'.format(t)) @@ -435,13 +493,10 @@ async def add(context, *, content): 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,['🔻','🌱','⏸','🏁','🔁'])} +# 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) @@ -454,21 +509,170 @@ async def a(context, *, content=""): # 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 +torStates = ('downloading', 'seeding', 'stopped', 'verifying', 'queued', 'finished', #0-5 + 'stalled', 'active', 'running', #6-8 + 'private', 'public', #9-10 + 'error', 'err_none', 'err_tracker_warn', 'err_tracker_error', 'err_local', # 11- +) +torStateEmoji = ('🔻','🌱','⏸','🩺','🚧','🏁', + '🐢','🐇','🚀', + '🔒','🔓', + '‼️','✅','⚠️','🌐','🖥' +) +torStateFilters = {i:"--filter {}".format(j) for i,j in zip(torStateEmoji,torStates)} +torStateFilters['↕️']='' -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,['🔻','🌱','⏸','🏁','🔁'])} +def numTorInState(torrents, state): + rpc_states = ('downloading', 'seeding', 'stopped', 'finished') + if state in rpc_states: + return len([True for t in torrents if t.status == state]) + elif state =='verifying': # these are also rpc statuses, but I want to combine them. + return len([True for t in torrents if 'check' in t.status]) + elif state == 'queued': + return len([True for t in torrents if 'load pending' in t.status]) + elif state == 'stalled': + return len([True for t in torrents if t.isStalled]) + elif state == 'active': + return len([True for t in torrents if not t.isStalled]) - len([True for t in torrents if t.rateDownload + t.rateUpload > 0]) + elif state == 'running': + return len([True for t in torrents if t.rateDownload + t.rateUpload > 0]) + elif state == 'private': + return len([True for t in torrents if t.isPrivate]) + elif state == 'public': + return len([True for t in torrents if not t.isPrivate]) + elif state == 'error': + return len([True for t in torrents if t.error != 0]) + elif state == 'err_none': + return len([True for t in torrents if t.error == 0]) + elif state == 'err_twarn': + return len([True for t in torrents if t.error == 1]) + elif state == 'err_terr': + return len([True for t in torrents if t.error == 2]) + elif state == 'err_local': + return len([True for t in torrents if t.error == 3]) + else: + return 0 + +def torSummary(torrents, repeat=False): + numInState = [numTorInState(torrents,s) for s in torStates] + 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 != 1 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(description="*React to see list of corresponding transfers*", color=0xb51a00) + embed.set_author(name="Torrent Summary 🌊", icon_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{}".format(numTot, 's' if numTot != 1 else ''), value=' '.join(['{} {}'.format(i,j) for i,j in zip(torStateEmoji[:6], numInState[:6])]), inline=False) + embed.add_field(name="{} Error{}".format(numInState[11], 's' if numInState[9] != 1 else ''), value='\n'.join(['{} {}'.format(i,"**{}**".format(j) if i != '✅' and j > 0 else j) for i,j in zip(torStateEmoji[12:], numInState[12:])]), inline=True) + embed.add_field(name="Activity", value='\n'.join(['{} {}'.format(i,j) for i,j in zip(torStateEmoji[6:9], numInState[6:9])]), inline=True) + embed.add_field(name="Tracker", value='\n'.join(['{} {}'.format(i,j) for i,j in zip(torStateEmoji[9:11], numInState[9:11])]), inline=True) + + embed.set_footer(text=topRatios+"\n📜 Symbol legend{}".format('\nUpdating every {} second{}—❎ to stop'.format(REPEAT_FREQ,'s' if REPEAT_FREQ != 1 else '') if repeat else ', 🔄 to auto-update')) + # await context.message.channel.send(embed=embed) + return embed,numInState + +@client.command(name='summary',aliases=['s'], pass_context=True) +async def summary(context, *, content="", repeat=False): + global REPEAT_COMMAND, REPEAT_MSG_LIST + if await CommandPrecheck(context): + if not repeat: + await context.message.delete() + stateEmoji = ('📜','❎' if repeat else '🔄','↕️') + torStateEmoji + ignoreEmoji = ('✅') + + summaryData=torSummary(TSCLIENT.get_torrents(), repeat=repeat) + + if repeat: + msg = REPEAT_MSG_LIST[0] + await msg.edit(embed=summaryData[0]) + else: + msg = await context.message.channel.send(embed=summaryData[0]) + + msgRxns = [str(r.emoji) for r in msg.reactions] + + for i in stateEmoji[:2]: + if i not in msgRxns: + await msg.add_reaction(i) + for i in range(len(summaryData[1])): + if summaryData[1][i] > 0 and stateEmoji[i+3] not in ignoreEmoji and stateEmoji[i+3] not in msgRxns: + await msg.add_reaction(stateEmoji[i+3]) + elif summaryData[1][i] == 0 and stateEmoji[i+3] in msgRxns: + await msg.clear_reaction(stateEmoji[i+3]) + + def check(reaction, user): + return user == context.message.author and reaction.message.id == msg.id and str(reaction.emoji) in stateEmoji + + try: + reaction, user = await client.wait_for('reaction_add', timeout=60.0 if not repeat else REPEAT_FREQ, check=check) + except asyncio.TimeoutError: + pass + else: + if str(reaction.emoji) in stateEmoji[2:] and str(reaction.emoji) not in ignoreEmoji: + await list_transfers(context, content=torStateFilters[str(reaction.emoji)]) + elif str(reaction.emoji) == stateEmoji[0]: + await legend(context) + elif str(reaction.emoji) == stateEmoji[1]: + if repeat: + REPEAT_COMMAND = False + REPEAT_MSG_LIST = [] + await context.message.channel.send("❎ Auto-update cancelled...") + else: + REPEAT_MSG_LIST = [msg] + await msg.clear_reaction('🔄') + await repeat_command(summary, context=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 + + +def torList(torrents, author_name="Torrent Transfers",title=None,description=None,repeat=False): + states = ('downloading', 'seeding', 'stopped', 'finished','checking','check pending','download pending','upload pending') + 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 '🔓') + out = "{} {} {} {} ".format(stateEmoji[t.status],errorStrs[t.error],'🚀' if t.rateDownload + t.rateUpload > 0 else '🐢' 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': @@ -478,6 +682,9 @@ def torList(torrents, author_name="Torrent Transfers",title=None,description=Non elif t.status == 'finished': out += "{} ⚖️ {:.2f}".format(humanbytes(t.totalSize),t.uploadRatio) + if t.error != 0: + out += "\n**Error:** *{}*".format(t.errorString) + return out nameList = ["{}) {:.245}{}".format(t.id,t.name,"..." if len(t.name) > 245 else "") for t in torrents] @@ -542,133 +749,102 @@ def torGetListOpsFromStr(listOpStr): if filter_regex == "": filter_regex = None - if filter_by is not None and filter_by not in filter_names: + if filter_by is not None and filter_by not in filter_names_full: 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=""): +async def repeat_command(command, context, content=""): + global REPEAT_COMMAND, REPEAT_MSG_LIST + REPEAT_COMMAND = True + start_time = datetime.datetime.now() + while REPEAT_COMMAND: + delta = datetime.datetime.now() - start_time + if delta.seconds >= REPEAT_TIMEOUT: + await context.message.channel.send("❎ Auto-update timed out...") + REPEAT_COMMAND = False + REPEAT_MSG_LIST = [] + return + # for msg in REPEAT_MSG_LIST: + # await msg.delete() + await command(context=context, content=content, repeat=True) + return + +@client.command(name='list', aliases=['l'], pass_context=True) +async def list_transfers(context, *, content="", repeat=False): + global REPEAT_COMMAND, REPEAT_MSG_LIST 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))) + await context.message.channel.send("Invalid filter specified. Choose one of {}".format(str(filter_names_full))) return if sort_by == -1: await context.message.channel.send("Invalid sort specified. Choose one of {}".format(str(sort_names))) return + if not repeat: + await context.message.delete() + 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: - msg = await context.message.channel.send(embed=e) + embeds[-1].set_footer(text="📜 Symbol legend{}".format('\nUpdating every {} second{}—❎ to stop'.format(REPEAT_FREQ,'s' if REPEAT_FREQ != 1 else '') if repeat else ', 🔄 to auto-update')) - await msg.add_reaction('📜') + if repeat: + msgs = REPEAT_MSG_LIST + for i,e in enumerate(embeds): + if i < len(msgs): + await msgs[i].edit(embed=e) + if i < len(embeds) and len(msgs[i].reactions) > 0: + await msgs[i].clear_reactions() + else: + msgs.append(await context.message.channel.send(embed=e)) + if len(msgs) > len(embeds): + for i in range(len(msgs) - len(embeds)): + await msgs[-1].delete() + del msgs[-1] + REPEAT_MSG_LIST = msgs + rxnEmoji = ['📜','❎'] + else: + msgs = [await context.message.channel.send(embed=e) for e in embeds] + rxnEmoji = ['📜','🔄'] + + msg = msgs[-1] + + msgRxns = msg.reactions + for r in msgRxns: + if str(r.emoji) not in rxnEmoji: + await msg.clear_reaction(r) + + msgRxns = [str(r.emoji) for r in msgRxns] + for e in rxnEmoji: + if e not in msgRxns: + await msg.add_reaction(e) def check(reaction, user): - return user == context.message.author and str(reaction.emoji) == '📜' + return user == context.message.author and reaction.message.id == msg.id and str(reaction.emoji) in rxnEmoji try: - reaction, user = await client.wait_for('reaction_add', timeout=60.0, check=check) + reaction, user = await client.wait_for('reaction_add', timeout=60.0 if not repeat else REPEAT_FREQ, check=check) except asyncio.TimeoutError: pass else: if str(reaction.emoji) == '📜': await legend(context) -@client.command(name='l', pass_context=True) -async def l(context, *, content=""): - await list(context, content=content) + elif str(reaction.emoji) == rxnEmoji[-1]: + if repeat: + REPEAT_COMMAND = False + REPEAT_MSG_LIST = [] + await context.message.channel.send("❎ Auto-update cancelled...") + else: + REPEAT_MSG_LIST = msgs + await msg.clear_reaction('🔄') + await repeat_command(list_transfers, context=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📜 Symbol legend") - # 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]) - for i in stateEmoji[-2:]: - await msg.add_reaction(i) - - 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: - if str(reaction.emoji) in stateEmoji[:-1]: - 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)]) - elif str(reaction.emoji) == stateEmoji[-1]: - await legend(context) -@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) +@client.command(name='modify', aliases=['m'], pass_context=True) async def modify(context, *, content=""): if await CommandPrecheck(context): allOnly = content.strip() == "" @@ -681,27 +857,28 @@ async def modify(context, *, content=""): 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))) + await context.message.channel.send("Invalid filter specified. Choose one of {}".format(str(filter_names_full))) 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 + + await context.message.delete() + 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" + ops = ["pause","resume","remove","removedelete","verify"] + opNames = ["pause","resume","remove","remove and delete","verify"] + opEmoji = ['⏸','▶️','❌','🗑','🩺'] + opStr = "⏸pause ▶️resume ❌remove 🗑remove  and  delete 🩺verify" 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=opStr) else: embed=discord.Embed(title="Modify transfers",color=0xb51a00) embed.set_author(name="No matching transfers found!", icon_url=LOGO_URL) embeds = [embed] else: + await context.message.delete() ops = ["pauseall","resumeall"] opNames = ["pause all","resume all"] opEmoji = ['⏸','▶️'] @@ -732,7 +909,9 @@ async def modify(context, *, content=""): await legend(context) elif str(reaction.emoji) in opEmoji[:-1]: cmds = {i:j for i,j in zip(opEmoji,ops)} + cmdNames = {i:j for i,j in zip(opEmoji,opNames)} cmd = cmds[str(reaction.emoji)] + cmdName = cmdNames[str(reaction.emoji)] doContinue = True if "remove" in cmds[str(reaction.emoji)]: @@ -752,34 +931,34 @@ async def modify(context, *, content=""): else: doContinue = str(reaction.emoji) == '✅' if doContinue: + await context.message.channel.send("{} Trying to {} transfer{}, please wait...".format(str(reaction.emoji), cmdName, 's' if allOnly or len(torrents) > 1 else '')) if "pause" in cmd: stop_torrents(torrents) elif "resume" in cmd: resume_torrents(torrents) + elif "verify" in cmd: + verify_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 = ["pause","resume","remove","removedelete","pauseall","resumeall","verify"] + opNames = ["paused","resumed","removed","removed and deleted","paused","resumed","queued for verification"] + 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.command(name='legend', pass_context=True) async def legend(context): embed = discord.Embed(title='Symbol legend', color=0xb51a00) - embed.add_field(name="Status", value="🔻—downloading\n🌱—seeding\n🏁—finished\n⏸—paused\n🔁—any", inline=True) + embed.add_field(name="Status", value="🔻—downloading\n🌱—seeding\n⏸—paused\n🩺—verifying\n🚧—queued\n🏁—finished\n↕️—any", inline=True) embed.add_field(name="Error", value="✅—none\n⚠️—tracker  warning\n🌐—tracker  error\n🖥—local  error", inline=True) - embed.add_field(name="Metrics", value="⬇️—(total)  download  rate\n⬆️—(total)  upload  rate\n⏬—total  downloaded\n⏫—total  uploaded\n⚖️—seed  ratio", inline=True) - embed.add_field(name="Timeout", value="🐢—stalled\n🐇—running", inline=True) + embed.add_field(name="Metrics", value="⬇️—download  rate\n⬆️—upload  rate\n⏬—total  downloaded\n⏫—total  uploaded\n⚖️—seed  ratio", inline=True) + embed.add_field(name="Activity", value="🐢—stalled\n🐇—active\n🚀—running (rate>0)", inline=True) embed.add_field(name="Tracker", value="🔒—private\n🔓—public", inline=True) - embed.add_field(name="Modifications", value="⏸—pause\n▶️—resume\n❌—remove\n🗑—remove  and  delete", inline=True) + embed.add_field(name="Modifications", value="⏸—pause\n▶️—resume\n❌—remove\n🗑—remove  and  delete\n🩺—verify", inline=True) await context.message.channel.send(embed=embed) return @@ -793,7 +972,7 @@ async def help(context, *, content=""): 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="Filtering", value='`--filter FILTER` or `-f FILTER`\n`FILTER` is one of `{}`'.format(str(filter_names_full)), 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)