class TransmissionClient(BTClientBase): def __init__(self, rpc_address, rpc_port, username, password, config={'path': '/transmission/rpc'}): """ Transmission Client for AutoPT config: dict {path: '/transmission/'} """ self.rpc_address = rpc_address self.rpc_port = int(rpc_port) self.username = username self.password = password self.path = config['path'] # For now, only consider path self.connected = False self.client = None def connect(self): try: self.client = Client(host=self.rpc_address, port=self.rpc_port, username=self.username, password=self.password, path=self.path) self.connected = True ret = ClientRet(ret_type=2) except: ret = ClientRet(ret_type=-2) finally: return ret def add_torrent(self, torrent_path, download_path=None): if not self.connected: ret = ClientRet(ret_type=-2) return ret abs_torrent_path = str(Path(torrent_path).resolve()) try: if download_path is None: torrent_obj = self.client.add_torrent(abs_torrent_path, paused=False) else: download_path = str(Path(download_path).resolve()) torrent_obj = self.client.add_torrent( abs_torrent_path, download_dir=download_path, paused=False) # Must be absolute path if torrent_obj is not None: ret = ClientRet(ret_type=3, ret_value=torrent_obj.hashString) except: ret = ClientRet(ret_type=-3) finally: return ret def list_torrents(self): if not self.connected: ret = ClientRet(ret_type=-2) return ret try: torrent_obj_list = self.client.get_torrents() session_status = {} for torrent_obj in torrent_obj_list: is_finished = math.isclose(torrent_obj.progress, 100) torrent_status = TorrentStatus( torrent_id=torrent_obj.hashString, is_finished=is_finished, name=torrent_obj.name) session_status[torrent_obj.hashString] = torrent_status ret = ClientRet(ret_type=4, ret_value=session_status) except: ret = ClientRet(ret_type=-4) finally: return ret def get_torrent_status(self, idx): if not self.connected: ret = ClientRet(ret_type=-2) return ret try: torrent_obj = self.client.get_torrent(idx) is_finished = math.isclose(torrent_obj.progress, 100) torrent_status = TorrentStatus(torrent_id=torrent_obj.hashString, is_finished=is_finished, name=torrent_obj.name) ret = ClientRet(ret_type=6, ret_value=torrent_status) except: ret = ClientRet(ret_type=-6) finally: return ret def del_torrent(self, idx, remove_data=True): if not self.connected: ret = ClientRet(ret_type=-2) return ret try: torrent_obj = self.client.get_torrent(idx) torrent_exist = True except: torrent_exist = False if torrent_exist: try: self.client.remove_torrent(idx, delete_data=remove_data) ret = ClientRet(ret_type=5) except: ret = ClientRet(ret_type=-5) finally: return ret else: ret = ClientRet(ret_type=-5) return ret def disconnect(self): self.connected = False self.client = None ret = ClientRet(ret_type=0) return ret
class TorrentService: def __init__(self, address, port, username, password): self.client = Client(address=address, port=port, username=username, password=password) def torrent_icon(self, status): if status == 'stopped': return '⏹' else: return '▶️' def torrent_description(self, torrent): return '{} {} {:01.0f}%, {}'.format(self.torrent_icon(torrent.status), torrent.status, torrent.progress, torrent.name) def add_torrent(self, url, path): tor = self.client.add_torrent(url, download_dir=path) return tor.name def torrent_list(self, update, context): torrents = self.client.get_torrents() torrents = sorted(torrents, key=lambda i: i.queue_position) keyboard = [] for torrent in torrents: tname = self.torrent_description(torrent) keyboard.append([ InlineKeyboardButton(tname, callback_data='t_info_{}'.format( torrent.id)) ]) keyboard.append( [InlineKeyboardButton('✕ Cancel', callback_data='cancel')]) reply_markup = InlineKeyboardMarkup(keyboard) update.message.reply_text(text='Torrents', reply_markup=reply_markup) return TORRENTLIST def torrent_info(self, update, context): query = update.callback_query tid = re.search('^t_info_(\d+?)$', query.data).group(1) tor = self.client.get_torrent(tid) stop_resume = [] if tor.status == 'stopped': stop_resume = [ InlineKeyboardButton('▷ Resume', callback_data='t_resume_{}'.format(tid)), InlineKeyboardButton( '▶︎ Resume now', callback_data='t_resume_now_{}'.format(tid)) ] elif tor.status == 'download pending': stop_resume = [ InlineKeyboardButton( '▶︎ Resume now', callback_data='t_resume_now_{}'.format(tid)), InlineKeyboardButton('◼︎ Stop', callback_data='t_stop_{}'.format(tid)) ] else: stop_resume = [ InlineKeyboardButton('◼︎ Stop', callback_data='t_stop_{}'.format(tid)) ] keyboard = [[ InlineKeyboardButton('▲ Top', callback_data='t_top_{}'.format(tid)), InlineKeyboardButton('△ Up', callback_data='t_up_{}'.format(tid)), InlineKeyboardButton('▽ Down', callback_data='t_down_{}'.format(tid)), InlineKeyboardButton('▼ Bottom', callback_data='t_bottom_{}'.format(tid)) ], stop_resume, [ InlineKeyboardButton( '◉ Verify', callback_data='t_verify_{}'.format(tid)) ], [ InlineKeyboardButton( '☒ Remove', callback_data='t_remove_confirm_{}'.format(tid)) ], [InlineKeyboardButton('✕ Cancel', callback_data='cancel')]] reply_markup = InlineKeyboardMarkup(keyboard) bot = context.bot bot.edit_message_text(chat_id=query.message.chat_id, message_id=query.message.message_id, reply_markup=reply_markup, text=self.torrent_description(tor)) return TORRENTINFO def move_up(self, update, context): query = update.callback_query tid = re.search('^t_up_(\d+?)$', query.data).group(1) self.client.queue_up(tid) tor = self.client.get_torrent(tid) context.bot.edit_message_text(chat_id=query.message.chat_id, message_id=query.message.message_id, text='{} has been moved up'.format( tor.name)) return ConversationHandler.END def move_down(self, update, context): query = update.callback_query tid = re.search('^t_down_(\d+?)$', query.data).group(1) self.client.queue_down(tid) tor = self.client.get_torrent(tid) context.bot.edit_message_text(chat_id=query.message.chat_id, message_id=query.message.message_id, text='{} has been moved down'.format( tor.name)) return ConversationHandler.END def move_top(self, update, context): query = update.callback_query tid = re.search('^t_top_(\d+?)$', query.data).group(1) self.client.queue_top(tid) tor = self.client.get_torrent(tid) context.bot.edit_message_text(chat_id=query.message.chat_id, message_id=query.message.message_id, text='{} has been moved to top'.format( tor.name)) return ConversationHandler.END def move_bottom(self, update, context): query = update.callback_query tid = re.search('^t_bottom_(\d+?)$', query.data).group(1) self.client.queue_bottom(tid) tor = self.client.get_torrent(tid) context.bot.edit_message_text( chat_id=query.message.chat_id, message_id=query.message.message_id, text='{} has been moved to bottom'.format(tor.name)) return ConversationHandler.END def stop_torrent(self, update, context): tid = re.search('^t_stop_(\d+?)$', update.callback_query.data).group(1) tor = self.client.get_torrent(tid) self.client.stop_torrent(tid) context.bot.edit_message_text( chat_id=update.callback_query.message.chat_id, message_id=update.callback_query.message.message_id, text='{} has been stopped'.format(tor.name)) return ConversationHandler.END def resume_torrent(self, update, context): tid = re.search('^t_resume_(\d+?)$', update.callback_query.data).group(1) tor = self.client.get_torrent(tid) self.client.start_torrent(tid) context.bot.edit_message_text( chat_id=update.callback_query.message.chat_id, message_id=update.callback_query.message.message_id, text='{} has been resumed'.format(tor.name)) return ConversationHandler.END def resume_torrent_now(self, update, context): tid = re.search('^t_resume_now_(\d+?)$', update.callback_query.data).group(1) tor = self.client.get_torrent(tid) self.client.start_torrent(tid, bypass_queue=True) context.bot.edit_message_text( chat_id=update.callback_query.message.chat_id, message_id=update.callback_query.message.message_id, text='{} has been resumed'.format(tor.name)) return ConversationHandler.END def verify_torrent(self, update, context): tid = re.search('^t_verify_(\d+?)$', update.callback_query.data).group(1) tor = self.client.get_torrent(tid) self.client.verify_torrent(tid) context.bot.edit_message_text( chat_id=update.callback_query.message.chat_id, message_id=update.callback_query.message.message_id, text='{} verification has been started'.format(tor.name)) return ConversationHandler.END def remove_torrent_confirm(self, update, context): tid = re.search('^t_remove_confirm_(\d+?)$', update.callback_query.data).group(1) tor = self.client.get_torrent(tid) keyboard = [[ InlineKeyboardButton('☒ Remove torrent', callback_data='t_remove_{}'.format(tid)) ], [ InlineKeyboardButton( '⌧ Remove torrent and delete files', callback_data='t_remove_complete_{}'.format(tid)) ], [InlineKeyboardButton('✕ Cancel', callback_data='cancel')]] reply_markup = InlineKeyboardMarkup(keyboard) context.bot.edit_message_text( chat_id=update.callback_query.message.chat_id, message_id=update.callback_query.message.message_id, reply_markup=reply_markup, text='Are you sure you want to remove {}?'.format(tor.name)) return REMOVETORRENT def remove_torrent(self, update, context): tid = re.search('^t_remove_(\d+?)$', update.callback_query.data).group(1) tor = self.client.get_torrent(tid) self.client.remove_torrent(tid) context.bot.edit_message_text( chat_id=update.callback_query.message.chat_id, message_id=update.callback_query.message.message_id, text='{} has been removed'.format(tor.name)) return ConversationHandler.END def remove_torrent_complete(self, update, context): tid = re.search('^t_remove_complete_(\d+?)$', update.callback_query.data).group(1) tor = self.client.get_torrent(tid) self.client.remove_torrent(tid, delete_data=True) context.bot.edit_message_text( chat_id=update.callback_query.message.chat_id, message_id=update.callback_query.message.message_id, text='{} has been removed'.format(tor.name)) return ConversationHandler.END
class TransmissionPrioritizer(object): def __init__(self, host, port, user, passwd): self._host = host self._port = port self._user = user self._pass = passwd logger.debug( 'Connecting to Transmission at %s:%s as user %s', host, port, user ) self._client = Client( address=host, port=port, user=user, password=passwd ) logger.debug('Connected to Transmission') def run(self, batch=2, rm_finished=False): logger.debug('Getting current torrents...') torrents = self._get_active_torrents() logger.info('Found %d active torrent(s)...', len(torrents)) for t in torrents: self._set_file_priority(t, batch) logger.info('Done.') if rm_finished: self._rm_finished_torrents() def _set_file_priority(self, torrent, batch): t_id = torrent._fields['id'].value logger.info( 'Checking files in torrent %d (%s)', t_id, torrent._get_name_string() ) files = self._client.get_files(ids=[t_id])[t_id] logger.debug('Torrent has %d files', len(files)) incomplete = [] for _id in sorted(files.keys(), key=lambda x: files[x]['name']): pct = (files[_id]['completed'] / files[_id]['size']) * 100 logger.debug( 'File %d: %s - %.2f%% complete - %s, priority %s', _id, files[_id]['name'], pct, 'selected' if files[_id]['selected'] else 'unselected', files[_id]['priority'] ) if pct < 100: incomplete.append(_id) logger.debug('%d files in torrent are incomplete', len(incomplete)) if len(incomplete) > batch: selected = incomplete[:batch] else: selected = incomplete logger.debug('First %d incomplete files: %s', len(selected), selected) data = {t_id: {}} for _id in files: data[t_id][_id] = { 'selected': files[_id]['selected'], 'priority': 'high' if _id in selected else 'normal' } logger.info( 'Ensuring high priority on first %d incomplete files: %s', len(selected), ', '.join([ '%d (%s)' % (x, files[x]['name']) for x in selected ]) ) logger.debug('set_files: %s', data) self._client.set_files(data) def _get_active_torrents(self): r = self._client.get_torrents() active = [] for t in r: logger.debug( 'Torrent %s (%s) - %s, %.2f%% complete', t._fields['id'].value, t._get_name_string(), t.status, t.progress ) if t.status in ['downloading', 'download pending']: active.append(t) logger.debug('%d of %d torrents active', len(active), len(r)) return active def _rm_finished_torrents(self): logger.debug('Looking for finished torrents to remove...') r = self._client.get_torrents() active = [] for t in r: logger.debug( 'Torrent %s (%s) - %s, %.2f%% complete', t._fields['id'].value, t._get_name_string(), t.status, t.progress ) if t.status != 'seeding' or t.progress != 100: continue logger.info( 'Removing finished/seeding torrent: %s (%s)', t._fields['id'].value, t._get_name_string() ) self._client.remove_torrent(t._fields['id'].value)
class TransmissionPrioritizer(object): def __init__(self, host, port, user, passwd): self._host = host self._port = port self._user = user self._pass = passwd logger.debug('Connecting to Transmission at %s:%s as user %s', host, port, user) self._client = Client(address=host, port=port, user=user, password=passwd) logger.debug('Connected to Transmission') def run(self, batch=2, rm_finished=False): logger.debug('Getting current torrents...') torrents = self._get_active_torrents() logger.info('Found %d active torrent(s)...', len(torrents)) for t in torrents: self._set_file_priority(t, batch) logger.info('Done.') if rm_finished: self._rm_finished_torrents() def _set_file_priority(self, torrent, batch): t_id = torrent._fields['id'].value logger.info('Checking files in torrent %d (%s)', t_id, torrent._get_name_string()) files = self._client.get_files(ids=[t_id])[t_id] logger.debug('Torrent has %d files', len(files)) incomplete = [] for _id in sorted(files.keys(), key=lambda x: files[x]['name']): pct = (files[_id]['completed'] / files[_id]['size']) * 100 logger.debug( 'File %d: %s - %.2f%% complete - %s, priority %s', _id, files[_id]['name'], pct, 'selected' if files[_id]['selected'] else 'unselected', files[_id]['priority']) if pct < 100: incomplete.append(_id) logger.debug('%d files in torrent are incomplete', len(incomplete)) if len(incomplete) > batch: selected = incomplete[:batch] else: selected = incomplete logger.debug('First %d incomplete files: %s', len(selected), selected) data = {t_id: {}} for _id in files: data[t_id][_id] = { 'selected': files[_id]['selected'], 'priority': 'high' if _id in selected else 'normal' } logger.info( 'Ensuring high priority on first %d incomplete files: %s', len(selected), ', '.join(['%d (%s)' % (x, files[x]['name']) for x in selected])) logger.debug('set_files: %s', data) self._client.set_files(data) def _get_active_torrents(self): r = self._client.get_torrents() active = [] for t in r: logger.debug('Torrent %s (%s) - %s, %.2f%% complete', t._fields['id'].value, t._get_name_string(), t.status, t.progress) if t.status in ['downloading', 'download pending']: active.append(t) logger.debug('%d of %d torrents active', len(active), len(r)) return active def _rm_finished_torrents(self): logger.debug('Looking for finished torrents to remove...') r = self._client.get_torrents() active = [] for t in r: logger.debug('Torrent %s (%s) - %s, %.2f%% complete', t._fields['id'].value, t._get_name_string(), t.status, t.progress) if t.status != 'seeding' or t.progress != 100: continue logger.info('Removing finished/seeding torrent: %s (%s)', t._fields['id'].value, t._get_name_string()) self._client.remove_torrent(t._fields['id'].value)
class TransmissionRpcClient(TorrentClient): def __init__(self, url: str, port: int, user: str, password: str): super().__init__(url, port, user, password) parse = urlparse(url) self.client_args = { "protocol": parse.scheme, "host": parse.netloc, "port": port, "path": parse.path, "username": user, "password": password, } # print(self.client_args) self.session: Client = None def connect(self): self.session = Client(**self.client_args) def disconnect(self): pass def add_torrent(self, file: bytes, paused: bool, path: str = None) -> str: if self.session is None: raise ConnectionError() send = BytesIO(file) args = { "torrent": send, "paused": paused, } if path is not None: args["download_dir"] = path torrent = self.session.add_torrent(**args) return torrent.id def del_torrent(self, torrent_id: str): self.session.remove_torrent(ids=torrent_id, delete_data=True) def get_torrent_status(self, torrent_id: str) -> TorrentStatus: torrent = self.session.get_torrent(torrent_id=torrent_id) return TransmissionRpcStatus(torrent, self.session) def get_torrents_status(self, torrent_id: List[str]) -> List[TorrentStatus]: torrents = self.session.get_torrents(ids=torrent_id) ret: List[TorrentStatus] = list() for torrent in torrents: ret.append(TransmissionRpcStatus(torrent, self.session)) return ret def start_torrent(self, torrent_id: str): torrent = self.session.get_torrent(torrent_id=torrent_id) torrent.start() def stop_torrent(self, torrent_id: str): torrent = self.session.get_torrent(torrent_id=torrent_id) torrent.stop()