diff options
Diffstat (limited to 'transmission-remote-cli')
-rwxr-xr-x | transmission-remote-cli | 275 |
1 files changed, 201 insertions, 74 deletions
diff --git a/transmission-remote-cli b/transmission-remote-cli index 75c20d3..da494c7 100755 --- a/transmission-remote-cli +++ b/transmission-remote-cli @@ -16,12 +16,12 @@ # http://www.gnu.org/licenses/gpl-3.0.txt # ######################################################################## -VERSION = '1.4.7' +VERSION = '1.5.0' TRNSM_VERSION_MIN = '1.90' -TRNSM_VERSION_MAX = '2.76' +TRNSM_VERSION_MAX = '2.80' RPC_VERSION_MIN = 8 -RPC_VERSION_MAX = 14 +RPC_VERSION_MAX = 15 # error codes CONNECTION_ERROR = 1 @@ -58,8 +58,9 @@ import locale import curses import curses.ascii from textwrap import wrap -from subprocess import call +from subprocess import call, Popen import netrc +import glob # optional features provided by non-standard modules @@ -114,6 +115,7 @@ config.set('Filtering', 'invert', 'False') config.add_section('Misc') config.set('Misc', 'compact_list', 'False') config.set('Misc', 'torrentname_is_progressbar', 'True') +config.set('Misc', 'file_viewer', 'xdg-open %%s') config.add_section('Colors') config.set('Colors', 'title_seed', 'bg:green,fg:black') config.set('Colors', 'title_download', 'bg:blue,fg:black') @@ -269,7 +271,6 @@ class Transmission: STATUS_DOWNLOAD = 4 # Downloading STATUS_SEED_WAIT = 5 # Queued to seed STATUS_SEED = 6 # Seeding - STATUS_ISOLATED = 7 # Torrent can't find peers TAG_TORRENT_LIST = 7 TAG_TORRENT_DETAILS = 77 @@ -336,6 +337,11 @@ class Transmission: Transmission.STATUS_SEED = 1 << 3 Transmission.STATUS_STOPPED = 1 << 4 + # Queue was implemented in Transmission v2.4 + if self.rpc_version >= 14: + self.LIST_FIELDS.append('queuePosition'); + self.DETAIL_FIELDS.append('queuePosition'); + # set up request list self.requests = {'torrent-list': TransmissionRequest(host, port, path, 'torrent-get', self.TAG_TORRENT_LIST, {'fields': self.LIST_FIELDS}), @@ -405,8 +411,7 @@ class Transmission: t['leechers'] = max(map(lambda x: x['leecherCount'], t['trackerStats'])) except ValueError: t['seeders'] = t['leechers'] = -1 - if not self.can_has_peers(t): - t['status'] = Transmission.STATUS_ISOLATED + t['isIsolated'] = not self.can_has_peers(t) if response['tag'] == self.TAG_TORRENT_LIST: self.torrent_cache = response['arguments']['torrents'] @@ -441,28 +446,32 @@ class Transmission: self.peer_progress_cache[peerid] = {'last_progress':peer['progress'], 'last_update':time.time(), 'download_speed':0, 'time_left':0} + this_peer = self.peer_progress_cache[peerid] + this_torrent = self.torrent_details_cache + # estimate how fast a peer is downloading if peer['progress'] < 1: this_time = time.time() - time_diff = this_time - self.peer_progress_cache[peerid]['last_update'] - progress_diff = peer['progress'] - self.peer_progress_cache[peerid]['last_progress'] - if self.peer_progress_cache[peerid]['last_progress'] and progress_diff > 0 and time_diff > 5: - downloaded = self.torrent_details_cache['totalSize'] * progress_diff - avg_speed = downloaded / time_diff - - if self.peer_progress_cache[peerid]['download_speed'] > 0: # make it less jumpy - avg_speed = (self.peer_progress_cache[peerid]['download_speed'] + avg_speed) /2 - - download_left = self.torrent_details_cache['totalSize'] - \ - (self.torrent_details_cache['totalSize']*peer['progress']) - time_left = download_left / avg_speed - - self.peer_progress_cache[peerid]['last_update'] = this_time # remember update time - self.peer_progress_cache[peerid]['download_speed'] = avg_speed - self.peer_progress_cache[peerid]['time_left'] = time_left - - self.peer_progress_cache[peerid]['last_progress'] = peer['progress'] # remember progress - self.torrent_details_cache['peers'][index].update(self.peer_progress_cache[peerid]) + time_diff = this_time - this_peer['last_update'] + progress_diff = peer['progress'] - this_peer['last_progress'] + if this_peer['last_progress'] and progress_diff > 0 and time_diff > 5: + download_left = this_torrent['totalSize'] - \ + (this_torrent['totalSize']*peer['progress']) + downloaded = this_torrent['totalSize'] * progress_diff + + this_peer['download_speed'] = \ + norm.add(peerid+':download_speed', downloaded/time_diff, 10) + this_peer['time_left'] = download_left/this_peer['download_speed'] + this_peer['last_update'] = this_time + + # infrequent progress updates lead to increasingly inaccurate + # estimates, so we go back to <guessing> + elif time_diff > 60: + this_peer['download_speed'] = 0 + this_peer['time_left'] = 0 + this_peer['last_update'] = time.time() + this_peer['last_progress'] = peer['progress'] # remember progress + this_torrent['peers'][index].update(this_peer) # resolve and locate peer's ip if features['dns'] and not self.hosts_cache.has_key(ip): @@ -591,13 +600,33 @@ class Transmission: request.send_request() self.wait_for_torrentlist_update() + def decrease_queue_position(self, torrent_id): + request = TransmissionRequest(self.host, self.port, self.path, 'queue-move-up', 1, + {'ids': [torrent_id]}) + request.send_request() + self.wait_for_torrentlist_update() + + def increase_queue_position(self, torrent_id): + request = TransmissionRequest(self.host, self.port, self.path, 'queue-move-down', 1, + {'ids': [torrent_id]}) + request.send_request() + self.wait_for_torrentlist_update() def toggle_turtle_mode(self): self.set_option('alt-speed-enabled', not self.status_cache['alt-speed-enabled']) def add_torrent(self, location): - request = TransmissionRequest(self.host, self.port, self.path, 'torrent-add', 1, {'filename': location}) + args = {} + try: + with open(location, 'rb') as fp: + args['metainfo'] = unicode(base64.b64encode(fp.read())) + # If the file doesnt exist or we cant open it, then it is either a url or needs to + # be open by the server + except IOError: + args['filename'] = location + + request = TransmissionRequest(self.host, self.port, self.path, 'torrent-add', 1, args) request.send_request() response = request.get_response() if response['result'] != 'success': @@ -730,16 +759,16 @@ class Transmission: status = 'verifying' elif torrent['status'] == Transmission.STATUS_CHECK_WAIT: status = 'will verify' + elif torrent['isIsolated']: + status = 'isolated' elif torrent['status'] == Transmission.STATUS_DOWNLOAD: status = ('idle','downloading')[torrent['rateDownload'] > 0] elif torrent['status'] == Transmission.STATUS_DOWNLOAD_WAIT: - status = 'will download' + status = 'will download (%d)' % torrent['queuePosition'] elif torrent['status'] == Transmission.STATUS_SEED: status = 'seeding' elif torrent['status'] == Transmission.STATUS_SEED_WAIT: - status = 'will seed' - elif torrent['status'] == Transmission.STATUS_ISOLATED: - status = 'isolated' + status = 'will seed (%d)' % torrent['queuePosition'] else: status = 'unknown state' return status @@ -749,27 +778,22 @@ class Transmission: recently, or if DHT is enabled for this torrent and globally, False otherwise. """ - # torrent has trackers + # Torrent has trackers? if torrent['trackerStats']: - has_connected = any([tracker['hasAnnounced'] or tracker['hasScraped'] - for tracker in torrent['trackerStats']]) - if has_connected: + # Did we try to connect a tracker? + if any([tracker['hasAnnounced'] for tracker in torrent['trackerStats']]): for tracker in torrent['trackerStats']: - if tracker['hasScraped'] and \ - tracker['lastScrapeTime'] >= tracker['lastAnnounceTime'] and \ - tracker['lastScrapeSucceeded']: - return True - elif tracker['hasAnnounced'] and \ - tracker['lastAnnounceTime'] > tracker['lastScrapeTime'] and \ - tracker['lastAnnounceSucceeded']: + if tracker['lastAnnounceSucceeded']: return True + # We didn't try yet; assume at least one is online else: - # If no tracker has been queried (yet), assume at least one is online return True - # torrent can make use of DHT + # Torrent can use DHT? + # ('dht-enabled' may be missing; assume DHT is available until we can say for sure) if not self.status_cache.has_key('dht-enabled') or \ (self.status_cache['dht-enabled'] and not torrent['isPrivate']): return True + # No ways of finding peers remaining return False def get_bandwidth_priority(self, torrent): @@ -798,6 +822,7 @@ class Interface: self.sort_orders = parse_sort_str(config.get('Sorting', 'order')) self.compact_list = config.getboolean('Misc', 'compact_list') self.torrentname_is_progressbar = config.getboolean('Misc', 'torrentname_is_progressbar') + self.file_viewer = config.get('Misc', 'file_viewer') self.torrents = server.get_torrent_list(self.sort_orders) self.stats = server.get_global_stats() @@ -832,7 +857,7 @@ class Interface: curses.KEY_BACKSPACE: self.leave_details, ord('q'): self.go_back_or_quit, ord('o'): self.o_key, - ord('\n'): self.select_torrent_detail_view, + ord('\n'): self.enter_key, curses.KEY_RIGHT: self.right_key, ord('l'): self.l_key, ord('s'): self.show_sort_order_menu, @@ -845,6 +870,8 @@ class Interface: ord('t'): self.t_key, ord('+'): self.bandwidth_priority, ord('-'): self.bandwidth_priority, + ord('J'): lambda c: self.move_queue('up'), + ord('K'): lambda c: self.move_queue('down'), ord('p'): self.pause_unpause_torrent, ord('P'): self.pause_unpause_all_torrent, ord('v'): self.verify_torrent, @@ -887,8 +914,13 @@ class Interface: ('status','S_tatus'), ('uploadedEver','Up_loaded'), ('rateUpload','_Upload Speed'), ('rateDownload','_Download Speed'), ('uploadRatio','_Ratio'), ('peersConnected','P_eers'), - ('downloadDir', 'L_ocation'), ('reverse','Re_verse') - ] + ('downloadDir', 'L_ocation') ] + + # queue was implemmented in transmission 2.4 + if server.get_rpc_version() >= 14: + self.sort_options.append(('queuePosition', '_Queue Position')) + + self.sort_options.append(('reverse','Re_verse')) try: @@ -965,15 +997,16 @@ class Interface: self.torrent_title_width -= self.rateDownload_width + 2 elif self.torrents: - visible_torrents = self.torrents[self.scrollpos/self.tlist_item_height : self.scrollpos/self.tlist_item_height + self.torrents_per_page + 1] - self.rateDownload_width = self.get_rateDownload_width(visible_torrents) - self.rateUpload_width = self.get_rateUpload_width(visible_torrents) - + self.visible_torrents_start = self.scrollpos/self.tlist_item_height + self.visible_torrents = self.torrents[self.visible_torrents_start : self.scrollpos/self.tlist_item_height + self.torrents_per_page + 1] + self.rateDownload_width = self.get_rateDownload_width(self.visible_torrents) + self.rateUpload_width = self.get_rateUpload_width(self.visible_torrents) self.torrent_title_width = self.width - self.rateUpload_width - 2 # show downloading column only if any downloading torrents are visible - if filter(lambda x: x['status']==Transmission.STATUS_DOWNLOAD, visible_torrents): + if filter(lambda x: x['status']==Transmission.STATUS_DOWNLOAD, self.visible_torrents): self.torrent_title_width -= self.rateDownload_width + 2 else: + self.visible_torrents = [] self.torrent_title_width = 80 def get_rateDownload_width(self, torrents): @@ -1067,7 +1100,7 @@ class Interface: self.select_unselect_file(c) # Torrent list elif self.selected_torrent == -1: - self.select_torrent_detail_view(c) + self.enter_key(c) def a_key(self, c): # File list @@ -1091,7 +1124,7 @@ class Interface: def l_key(self, c): if self.focus > -1 and self.selected_torrent == -1: - self.select_torrent_detail_view(c) + self.enter_key(c) elif self.selected_torrent > -1: self.file_pritority_or_switch_details(c) @@ -1117,25 +1150,30 @@ class Interface: def right_key(self, c): if self.focus > -1 and self.selected_torrent == -1: - self.select_torrent_detail_view(c) + self.enter_key(c) else: self.file_pritority_or_switch_details(c) def add_torrent(self): - location = self.dialog_input_text("Add torrent from file or URL", os.getcwd()) + location = self.dialog_input_text("Add torrent from file or URL", homedir2tilde(os.getcwd()+os.sep), tab_complete='all') if location: - error = server.add_torrent(location) + error = server.add_torrent(tilde2homedir(location)) if error: msg = wrap("Couldn't add torrent \"%s\":" % location) msg.extend(wrap(error, self.width-4)) self.dialog_ok("\n".join(msg)) - def select_torrent_detail_view(self, c): + def enter_key(self, c): + # Torrent list if self.focus > -1 and self.selected_torrent == -1: self.screen.clear() self.selected_torrent = self.focus server.set_torrent_details_id(self.torrents[self.focus]['id']) server.wait_for_details_update() + # File list + elif self.selected_torrent > -1 and self.details_category_focus == 1: + self.open_torrent_file(c) + def show_sort_order_menu(self, c): if self.selected_torrent == -1: @@ -1220,6 +1258,15 @@ class Interface: elif c == ord('+') and self.focus > -1: server.increase_bandwidth_priority(self.torrents[self.focus]['id']) + def move_queue(self, direction): + # queue was implemmented in Transmission v2.4 + if server.get_rpc_version() >= 14: + if direction == 'up' and self.focus > -1: + server.increase_queue_position(self.torrents[self.focus]['id']) + elif direction == 'down' and self.focus > -1: + server.decrease_queue_position(self.torrents[self.focus]['id']) + + def pause_unpause_torrent(self, c): if self.focus > -1: if self.selected_torrent > -1: @@ -1436,6 +1483,24 @@ class Interface: else: self.selected_files = range(0, len(self.torrent_details['files'])) + def open_torrent_file(self, c): + if self.focus_detaillist >= 0: + details = server.get_torrent_details() + file_server_index = self.file_index_map[self.focus_detaillist] + file_name = details['files'][file_server_index]['name'] + download_dir = details['downloadDir'] + + viewer_cmd=[] + for argstr in self.file_viewer.split(" "): + viewer_cmd.append(argstr.replace('%s',download_dir + file_name)) + try: + self.restore_screen() + call(viewer_cmd) + self.get_screen_size() + except OSError, err: + self.get_screen_size() + self.dialog_ok("%s:\n%s" % (" ".join(viewer_cmd), err)) + def move_in_details(self, c): if self.selected_torrent > -1: if c == ord("\t"): @@ -1459,7 +1524,7 @@ class Interface: if self.focus > -1: location = homedir2tilde(self.torrents[self.focus]['downloadDir']) msg = 'Move "%s" from\n%s to' % (self.torrents[self.focus]['name'], location) - path = self.dialog_input_text(msg, location) + path = self.dialog_input_text(msg, location, tab_complete='dirs') if path: server.move_torrent(self.torrents[self.focus]['id'], tilde2homedir(path)) @@ -1501,7 +1566,7 @@ class Interface: self.torrents = [t for t in self.torrents if t['status'] == Transmission.STATUS_CHECK \ or t['status'] == Transmission.STATUS_CHECK_WAIT] elif self.filter_list == 'isolated': - self.torrents = [t for t in self.torrents if t['status'] == Transmission.STATUS_ISOLATED] + self.torrents = [t for t in self.torrents if t['isIsolated']] # invert list? if self.filter_inverse: self.torrents = [t for t in unfiltered if t not in self.torrents] @@ -1556,13 +1621,13 @@ class Interface: self.manage_layout() ypos = 0 - for i in range(len(self.torrents)): - ypos += self.draw_torrentlist_item(self.torrents[i], - (i == self.focus), + for i in range(len(self.visible_torrents)): + ypos += self.draw_torrentlist_item(self.visible_torrents[i], + (i == self.focus-self.visible_torrents_start), self.compact_list, ypos) - self.pad.refresh(self.scrollpos,0, 1,0, self.mainview_height,self.width-1) + self.pad.refresh(0,0, 1,0, self.mainview_height,self.width-1) self.screen.refresh() @@ -1633,7 +1698,9 @@ class Interface: size = '| ' + size title = ljust_columns(torrent['name'], width - len(size)) + size - if torrent['status'] == Transmission.STATUS_SEED or \ + if torrent['isIsolated']: + color = curses.color_pair(self.colors.id('title_error')) + elif torrent['status'] == Transmission.STATUS_SEED or \ torrent['status'] == Transmission.STATUS_SEED_WAIT: color = curses.color_pair(self.colors.id('title_seed')) elif torrent['status'] == Transmission.STATUS_STOPPED: @@ -1641,8 +1708,6 @@ class Interface: elif torrent['status'] == Transmission.STATUS_CHECK or \ torrent['status'] == Transmission.STATUS_CHECK_WAIT: color = curses.color_pair(self.colors.id('title_verify')) - elif torrent['status'] == Transmission.STATUS_ISOLATED: - color = curses.color_pair(self.colors.id('title_error')) elif torrent['rateDownload'] == 0: color = curses.color_pair(self.colors.id('title_idle')) elif torrent['percentDone'] < 100: @@ -1672,7 +1737,7 @@ class Interface: peers = '' parts = [server.get_status(torrent)] - if torrent['status'] == Transmission.STATUS_ISOLATED and torrent['peersConnected'] <= 0: + if torrent['isIsolated'] and torrent['peersConnected'] <= 0: if not torrent['trackerStats']: parts[0] = "Unable to find peers without trackers and DHT disabled" else: @@ -1812,7 +1877,7 @@ class Interface: info[-1].append("no transmission in progress") info.append(['Ratio: ', '%.2f copies distributed' % copies_distributed]) - norm_upload_rate = norm.add('%s:rateUpload' % t['id'], t['rateUpload'], 15) + norm_upload_rate = norm.add('%s:rateUpload' % t['id'], t['rateUpload'], 50) if norm_upload_rate > 0: target_ratio = self.get_target_ratio() bytes_left = (max(t['downloadedEver'],t['sizeWhenDone']) * target_ratio) - t['uploadedEver'] @@ -2411,7 +2476,8 @@ class Interface: else: help = [('Move with','cursor keys'), ('q','Back to List')] if self.details_category_focus == 1 and self.focus_detaillist > -1: - help = [('space','(De)Select File'), + help = [('enter', 'Open File'), + ('space','(De)Select File'), ('left/right','De-/Increase Priority'), ('escape','Unfocus/-select')] + help elif self.details_category_focus == 2: @@ -2439,6 +2505,10 @@ class Interface: " a Add torrent\n" + \ " Del/r Remove torrent and keep content\n" + \ " Shift+Del/R Remove torrent and delete content\n" + + # Queue was implemented in Transmission v2.4 + if server.get_rpc_version() >= 14: + message += " K/J Increase/decrease Queue Position for focused torrent\n" # Torrent list if self.selected_torrent == -1: message += " / Search in torrent list\n" + \ @@ -2579,7 +2649,12 @@ class Interface: elif c == 27 or c == curses.KEY_BREAK: return -1 - def dialog_input_text(self, message, input='', on_change=None, on_enter=None): + + # tab_complete values: + # 'all': complete with any files/directories + # 'dirs': complete only with directories + # any false value: do not complete + def dialog_input_text(self, message, input='', on_change=None, on_enter=None, tab_complete=None): width = self.width - 4 textwidth = self.width - 8 height = message.count("\n") + 4 @@ -2617,6 +2692,11 @@ class Interface: elif index < len(input) and c == curses.ascii.ctrl(ord('k')): input = input[:index] if on_change: on_change(input) + elif c == curses.ascii.ctrl(ord('u')): + # Delete from cursor until beginning of line + input = input[index:] + index = 0 + if on_change: on_change(input) elif c == curses.KEY_HOME or c == curses.ascii.ctrl(ord('a')): index = 0 elif c == curses.KEY_END or c == curses.ascii.ctrl(ord('e')): @@ -2631,6 +2711,16 @@ class Interface: input = input[:index] + chr(c) + (index < len(input) and input[index:] or '') index += 1 if on_change: on_change(input) + elif c == ord('\t') and tab_complete: + possible_choices = glob.glob(tilde2homedir(input)+'*') + if tab_complete == 'dirs': + possible_choices = [ d for d in possible_choices if os.path.isdir(d) ] + if(possible_choices): + input = os.path.commonprefix(possible_choices) + if len(possible_choices) == 1 and os.path.isdir(input) and input.endswith(os.sep) == False: + input += os.sep + input = homedir2tilde(input) + index = len(input) if on_change: win.redrawwin() def dialog_search_torrentlist(self, c): @@ -2787,13 +2877,20 @@ class Interface: options.append(('_Micro Transport Protocol', ('disabled','enabled')[self.stats['utp-enabled']])) options.append(('_Global Peer Limit', "%d" % self.stats['peer-limit-global'])) options.append(('Peer Limit per _Torrent', "%d" % self.stats['peer-limit-per-torrent'])) - options.append(('_Seed Ratio Limit', "%s" % ('unlimited',self.stats['seedRatioLimit'])[self.stats['seedRatioLimited']])) options.append(('T_urtle Mode UL Limit', "%dK" % self.stats['alt-speed-up'])) options.append(('Tu_rtle Mode DL Limit', "%dK" % self.stats['alt-speed-down'])) + options.append(('_Seed Ratio Limit', "%s" % ('unlimited',self.stats['seedRatioLimit'])[self.stats['seedRatioLimited']])) + # queue was implemented in Transmission v2.4 + if server.get_rpc_version() >= 14: + options.append(('Do_wnload Queue Size', "%s" % ('disabled',self.stats['download-queue-size'])[self.stats['download-queue-enabled']])) + options.append(('S_eed Queue Size', "%s" % ('disabled',self.stats['seed-queue-size'])[self.stats['seed-queue-enabled']])) options.append(('Title is Progress _Bar', ('no','yes')[self.torrentname_is_progressbar])) + options.append(('File _Viewer', "%s" % self.file_viewer)) + max_len = max([sum([len(re.sub('_', '', x)) for x in y[0]]) for y in options]) - win = self.window(len(options)+2, max_len+15, '', "Global Options") + win_width = min(max(len(self.file_viewer)+5, 15), self.width+max_len) + win = self.window(len(options)+2, max_len+win_width, '', "Global Options") line_num = 1 for option in options: @@ -2869,6 +2966,36 @@ class Interface: server.set_option('alt-speed-down', limit) elif c == ord('b'): self.torrentname_is_progressbar = not self.torrentname_is_progressbar + # Queue was implemmented in Transmission v2.4 + elif c == ord('w') and server.get_rpc_version() >= 14: + queue_size = self.dialog_input_number('Download Queue size', + (0, self.stats['download-queue-size'])[self.stats['download-queue-enabled']], + allow_negative_one = False) + if queue_size != -128: + if queue_size == 0: + server.set_option('download-queue-enabled', False) + elif queue_size > 0: + if not self.stats['download-queue-enabled']: + server.set_option('download-queue-enabled', True) + server.set_option('download-queue-size', queue_size) + # Queue was implemmented in Transmission v2.4 + elif c == ord('e') and server.get_rpc_version() >= 14: + queue_size = self.dialog_input_number('Seed Queue size', + (0, self.stats['seed-queue-size'])[self.stats['seed-queue-enabled']], + allow_negative_one = False) + if queue_size != -128: + if queue_size == 0: + server.set_option('seed-queue-enabled', False) + elif queue_size > 0: + if not self.stats['seed-queue-enabled']: + server.set_option('seed-queue-enabled', True) + server.set_option('seed-queue-size', queue_size) + + elif c == ord('v'): + viewer = self.dialog_input_text('File Viewer\nExample: xdg-viewer %s', self.file_viewer) + if viewer: + config.set('Misc', 'file_viewer', viewer.replace('%s','%%s')) + self.file_viewer=viewer self.draw_torrent_list() @@ -3065,7 +3192,7 @@ def debug(data): if cmd_args.DEBUG: file = open("debug.log", 'a') if type(data) == type(str()): - file.write(self.enc(data)) + file.write(data) else: import pprint pp = pprint.PrettyPrinter(indent=4) |