From 322091b9506126cd06ecd3ba95aa73e3099f12d8 Mon Sep 17 00:00:00 2001 From: Jonathan McCrohan Date: Fri, 5 Jul 2013 20:28:01 +0100 Subject: Imported Upstream version 1.6.0 --- NEWS | 15 ++++ transmission-remote-cli | 231 ++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 200 insertions(+), 46 deletions(-) diff --git a/NEWS b/NEWS index 9e58c79..e9cbc6c 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,18 @@ +1.6.0 2013-06-28 + BUGFIXES: + - Fix timestamps after year 2038 on 32-bit systems + - Fix host placement in peer list + - Fallback to UTF-8 on LookupError from getpreferredencoding() + (as seen when ssh'ing into Mac OS) + + - Jump to and select directories in file list + - Sort torrents by tracker + - Make blank line between torrents optional + - Add preview for incomplete files + - Move queue position by 10 with S-Left/Right + - Show IP:PORT in peer list + + 1.5.0 2013-03-21 BUGFIXES: - Torrents should no longer become 'isolated' for seemingly no reason diff --git a/transmission-remote-cli b/transmission-remote-cli index da494c7..bbb3ea9 100755 --- a/transmission-remote-cli +++ b/transmission-remote-cli @@ -16,7 +16,7 @@ # http://www.gnu.org/licenses/gpl-3.0.txt # ######################################################################## -VERSION = '1.5.0' +VERSION = '1.6.0' TRNSM_VERSION_MIN = '1.90' TRNSM_VERSION_MAX = '2.80' @@ -42,6 +42,7 @@ except ImportError: quit("Please install simplejson or Python 2.6 or higher.") import time +import datetime import re import base64 import httplib @@ -61,6 +62,8 @@ from textwrap import wrap from subprocess import call, Popen import netrc import glob +import operator +import urlparse # optional features provided by non-standard modules @@ -114,8 +117,10 @@ config.set('Filtering', 'filter', '') config.set('Filtering', 'invert', 'False') config.add_section('Misc') config.set('Misc', 'compact_list', 'False') +config.set('Misc', 'blank_lines', 'True') config.set('Misc', 'torrentname_is_progressbar', 'True') config.set('Misc', 'file_viewer', 'xdg-open %%s') +config.set('Misc', 'file_open_in_terminal', 'True') config.add_section('Colors') config.set('Colors', 'title_seed', 'bg:green,fg:black') config.set('Colors', 'title_download', 'bg:blue,fg:black') @@ -397,6 +402,15 @@ class Transmission: def parse_response(self, response): + def get_main_tracker_domain(torrent): + if torrent['trackerStats']: + trackers = sorted(torrent['trackerStats'], + key=operator.itemgetter('tier', 'id')) + return urlparse.urlparse(trackers[0]['announce']).hostname + else: + # Trackerless torrents + return None + # response is a reply to torrent-get if response['tag'] == self.TAG_TORRENT_LIST or response['tag'] == self.TAG_TORRENT_DETAILS: for t in response['arguments']['torrents']: @@ -412,6 +426,7 @@ class Transmission: except ValueError: t['seeders'] = t['leechers'] = -1 t['isIsolated'] = not self.can_has_peers(t) + t['mainTrackerDomain'] = get_main_tracker_domain(t) if response['tag'] == self.TAG_TORRENT_LIST: self.torrent_cache = response['arguments']['torrents'] @@ -491,14 +506,15 @@ class Transmission: return self.status_cache def get_torrent_list(self, sort_orders): + def sort_value(value): + try: + return value.lower() + except AttributeError: + return value try: for sort_order in sort_orders: - if isinstance(self.torrent_cache[0][sort_order['name']], (str, unicode)): - self.torrent_cache.sort(key=lambda x: x[sort_order['name']].lower(), - reverse=sort_order['reverse']) - else: - self.torrent_cache.sort(key=lambda x: x[sort_order['name']], - reverse=sort_order['reverse']) + self.torrent_cache.sort(key=lambda x: sort_value(x[sort_order['name']]), + reverse=sort_order['reverse']) except IndexError: return [] return self.torrent_cache @@ -600,15 +616,17 @@ 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 move_queue(self, torrent_id, new_position): + args = {'ids': [ torrent_id ] } + if new_position in ('up', 'down', 'top', 'bottom'): + method_name = 'queue-move-' + new_position + elif isinstance(new_position, int): + method_name = 'torrent-set' + args['queuePosition'] = min(max(new_position, 0), len(self.torrent_cache)-1) + else: + raise ValueError("Is not up/down/top/bottom/: %s" % new_position) - def increase_queue_position(self, torrent_id): - request = TransmissionRequest(self.host, self.port, self.path, 'queue-move-down', 1, - {'ids': [torrent_id]}) + request = TransmissionRequest(self.host, self.port, self.path, method_name, 1, args) request.send_request() self.wait_for_torrentlist_update() @@ -821,8 +839,10 @@ class Interface: self.filter_inverse = config.getboolean('Filtering', 'invert') self.sort_orders = parse_sort_str(config.get('Sorting', 'order')) self.compact_list = config.getboolean('Misc', 'compact_list') + self.blank_lines = config.getboolean('Misc', 'blank_lines') self.torrentname_is_progressbar = config.getboolean('Misc', 'torrentname_is_progressbar') self.file_viewer = config.get('Misc', 'file_viewer') + self.file_open_in_terminal = config.getboolean('Misc', 'file_open_in_terminal') self.torrents = server.get_torrent_list(self.sort_orders) self.stats = server.get_global_stats() @@ -846,7 +866,7 @@ class Interface: self.exit_now = False locale.setlocale(locale.LC_ALL, '') - self.encoding = locale.getpreferredencoding() + self.encoding = locale.getpreferredencoding() or 'UTF-8' self.keybindings = { ord('?'): self.call_list_key_bindings, @@ -870,8 +890,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('J'): self.J_key, + ord('K'): self.K_key, ord('p'): self.pause_unpause_torrent, ord('P'): self.pause_unpause_all_torrent, ord('v'): self.verify_torrent, @@ -903,9 +923,14 @@ class Interface: curses.KEY_LEFT: self.file_pritority_or_switch_details, ord(' '): self.space_key, ord('a'): self.a_key, + ord('A'): self.A_key, ord('m'): self.move_torrent, ord('n'): self.reannounce_torrent, - ord('/'): self.dialog_search_torrentlist + ord('/'): self.dialog_search_torrentlist, + curses.KEY_SEND: lambda c: self.move_queue('bottom'), + curses.KEY_SHOME: lambda c: self.move_queue('top'), + curses.KEY_SLEFT: lambda c: self.move_queue('ppage'), + curses.KEY_SRIGHT: lambda c: self.move_queue('npage') } self.sort_options = [ @@ -914,7 +939,7 @@ class Interface: ('status','S_tatus'), ('uploadedEver','Up_loaded'), ('rateUpload','_Upload Speed'), ('rateDownload','_Download Speed'), ('uploadRatio','_Ratio'), ('peersConnected','P_eers'), - ('downloadDir', 'L_ocation') ] + ('downloadDir', 'L_ocation'), ('mainTrackerDomain', 'Trac_ker') ] # queue was implemmented in transmission 2.4 if server.get_rpc_version() >= 14: @@ -1024,7 +1049,8 @@ class Interface: return new_width def recalculate_torrents_per_page(self): - self.tlist_item_height = 3 if not self.compact_list else 1 + self.lines_per_entry = 3 if self.blank_lines else 2 + self.tlist_item_height = self.lines_per_entry if not self.compact_list else 1 self.mainview_height = self.height - 2 self.torrents_per_page = self.mainview_height / self.tlist_item_height @@ -1055,6 +1081,7 @@ class Interface: config.set('Filtering', 'filter', self.filter_list) config.set('Filtering', 'invert', str(self.filter_inverse)) config.set('Misc', 'compact_list', str(self.compact_list)) + config.set('Misc', 'blank_lines', str(self.blank_lines)) config.set('Misc', 'torrentname_is_progressbar', str(self.torrentname_is_progressbar)) save_config(cmd_args.configfile) return @@ -1102,6 +1129,11 @@ class Interface: elif self.selected_torrent == -1: self.enter_key(c) + def A_key(self, c): + # File list + if self.selected_torrent > -1 and self.details_category_focus == 1: + self.select_unselect_file(c) + def a_key(self, c): # File list if self.selected_torrent > -1 and self.details_category_focus == 1: @@ -1148,6 +1180,18 @@ class Interface: elif self.selected_torrent > -1 and self.details_category_focus == 3: self.remove_tracker() + def J_key(self, c): + if self.selected_torrent > -1 and self.details_category_focus == 1: + self.move_to_next_directory_in_filelist() + else: + self.move_queue('down') + + def K_key(self, c): + if self.selected_torrent > -1 and self.details_category_focus == 1: + self.move_to_previous_directory_in_filelist() + else: + self.move_queue('up') + def right_key(self, c): if self.focus > -1 and self.selected_torrent == -1: self.enter_key(c) @@ -1260,12 +1304,16 @@ class Interface: 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']) - + if server.get_rpc_version() >= 14 and self.focus > -1: + if direction in ('ppage', 'npage'): + new_position = self.torrents[self.focus]['queuePosition'] + if direction == 'ppage': + new_position -= 10 + else: + new_position += 10 + else: + new_position = direction + server.move_queue(self.torrents[self.focus]['id'], new_position) def pause_unpause_torrent(self, c): if self.focus > -1: @@ -1476,6 +1524,25 @@ class Interface: except ValueError: self.selected_files.append(self.focus_detaillist) curses.ungetch(curses.KEY_DOWN) # move down + # (un)select directory + elif c == ord('A'): + file_id = self.file_index_map[self.focus_detaillist] + focused_dir = os.path.dirname(self.torrent_details['files'][file_id]['name']) + if self.selected_files.count(self.focus_detaillist): + for focus in range(0, len(self.torrent_details['files'])): + file_id = self.file_index_map[focus] + if self.torrent_details['files'][file_id]['name'].startswith(focused_dir): + try: + while focus in self.selected_files: + self.selected_files.remove(focus) + except ValueError: + pass + else: + for focus in range(0, len(self.torrent_details['files'])): + file_id = self.file_index_map[focus] + if self.torrent_details['files'][file_id]['name'].startswith(focused_dir): + self.selected_files.append(focus) + self.move_to_next_directory_in_filelist() # (un)select all files elif c == ord('a'): if self.selected_files: @@ -1483,20 +1550,67 @@ class Interface: else: self.selected_files = range(0, len(self.torrent_details['files'])) + def move_to_next_directory_in_filelist(self): + if self.selected_torrent > -1 and self.details_category_focus == 1: + self.focus_detaillist = max(self.focus_detaillist, 0) + file_id = self.file_index_map[self.focus_detaillist] + focused_dir = os.path.dirname(self.torrent_details['files'][file_id]['name']) + while self.torrent_details['files'][file_id]['name'].startswith(focused_dir) \ + and self.focus_detaillist < len(self.torrent_details['files'])-1: + self.movement_keys(curses.KEY_DOWN) + file_id = self.file_index_map[self.focus_detaillist] + + def move_to_previous_directory_in_filelist(self): + if self.selected_torrent > -1 and self.details_category_focus == 1: + self.focus_detaillist = max(self.focus_detaillist, 0) + file_id = self.file_index_map[self.focus_detaillist] + focused_dir = os.path.dirname(self.torrent_details['files'][file_id]['name']) + while self.torrent_details['files'][file_id]['name'].startswith(focused_dir) \ + and self.focus_detaillist > 0: + self.movement_keys(curses.KEY_UP) + file_id = self.file_index_map[self.focus_detaillist] + def open_torrent_file(self, c): if self.focus_detaillist >= 0: details = server.get_torrent_details() + stats = server.get_global_stats() + file_server_index = self.file_index_map[self.focus_detaillist] file_name = details['files'][file_server_index]['name'] + download_dir = details['downloadDir'] + incomplete_dir = stats['incomplete-dir'] + '/' + + file_path = None + possible_file_locations = [ + download_dir + file_name, + download_dir + file_name + '.part', + incomplete_dir + file_name, + incomplete_dir + file_name + '.part' + ] + + for f in possible_file_locations: + if (os.path.isfile(f)): + file_path = f + break + + if file_path is None: + self.get_screen_size() + self.dialog_ok("Could not find file:\n%s" % (file_name)) + return viewer_cmd=[] for argstr in self.file_viewer.split(" "): - viewer_cmd.append(argstr.replace('%s',download_dir + file_name)) + viewer_cmd.append(argstr.replace('%s', file_path)) try: - self.restore_screen() - call(viewer_cmd) - self.get_screen_size() + if self.file_open_in_terminal: + self.restore_screen() + call(viewer_cmd) + self.get_screen_size() + else: + devnull = open(os.devnull, 'wb') + Popen(viewer_cmd, stdout=devnull, stderr=devnull) + devnull.close() except OSError, err: self.get_screen_size() self.dialog_ok("%s:\n%s" % (" ".join(viewer_cmd), err)) @@ -1649,7 +1763,7 @@ class Interface: self.draw_ratio(torrent, y) self.draw_torrentlist_status(torrent, focused, y) - return 3 # number of lines that were used for drawing the list item + return self.lines_per_entry # number of lines that were used for drawing the list item else: # Draw ratio in place of upload rate if upload rate = 0 if not torrent['rateUpload']: @@ -2077,10 +2191,14 @@ class Interface: same += 1 # Reduce depth for each directory f has less than current_folder - for i in range(current_folder_len - same): - current_depth -= 1 - filelist.append(' '*current_depth + ' '*31 + '/') - pos += 1 + if self.blank_lines: + for i in range(current_folder_len - same): + current_depth -= 1 + filelist.append(' '*current_depth + ' '*31 + '/') + pos += 1 + else: # code duplication, but less calculation + for i in range(current_folder_len - same): + current_depth -= 1 # Stepping out of a directory, but not into a new directory if f_len < current_folder_len and f_len == same: @@ -2116,15 +2234,17 @@ class Interface: # Find width of columns clientname_width = 0 address_width = 0 + port_width = 0 for peer in peers: if len(peer['clientName']) > clientname_width: clientname_width = len(peer['clientName']) if len(peer['address']) > address_width: address_width = len(peer['address']) + if len(str(peer['port'])) > port_width: port_width = len(str(peer['port'])) # Column names column_names = 'Flags %3d Down %3d Up Progress ETA ' % \ (self.torrent_details['peersSendingToUs'], self.torrent_details['peersGettingFromUs']) column_names += 'Client'.ljust(clientname_width + 1) \ - + 'Address'.ljust(address_width) + + 'Address'.ljust(address_width+port_width+1) if features['geoip']: column_names += 'Country' if features['dns']: column_names += ' Host' @@ -2175,8 +2295,9 @@ class Interface: self.pad.addstr(" ") # Client self.pad.addstr(self.enc(peer['clientName'].ljust(clientname_width + 1))) - # Address - self.pad.addstr(peer['address'].ljust(address_width + 1)) + # Address:Port + self.pad.addstr(peer['address'].rjust(address_width) + \ + ':' + str(peer['port']).ljust(port_width) + ' ') # Country if features['geoip']: self.pad.addstr(" %2s " % geo_ips[peer['address']]) # Host @@ -2493,8 +2614,8 @@ class Interface: def list_key_bindings(self): title = 'Help Menu' message = " F1/? Show this help\n" + \ - " u/d Adjust maximum global upload/download rate\n" + \ - " U/D Adjust maximum upload/download rate for focused torrent\n" + \ + " u/d Adjust maximum global up-/download rate\n" + \ + " U/D Adjust maximum up-/download rate for focused torrent\n" + \ " L Set seed ratio limit for focused torrent\n" + \ " +/- Adjust bandwidth priority for focused torrent\n" + \ " p Pause/Unpause torrent\n" + \ @@ -2507,8 +2628,12 @@ class Interface: " 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" + if server.get_rpc_version() >= 14 and self.details_category_focus != 1: + message += " J/K Move focused torrent in queue up/down\n" + \ + " Shift+Lft/Rght Move focused torrent in queue up/down by 10\n" + \ + " Shift+Home/End Move focused torrent to top/bottom of queue\n" + else: + message += " J/K Jump to next/previous directory\n" # Torrent list if self.selected_torrent == -1: message += " / Search in torrent list\n" + \ @@ -2550,6 +2675,7 @@ class Interface: message += " Up/Down Select file\n" + \ " Space Select/Deselect focused file\n" + \ " a Select/Deselect all files\n" + \ + " A Select/Deselect directory\n" + \ " Esc Unfocus+Unselect or Back to torrent list\n" + \ " q/Backspace Back to torrent list" else: @@ -2798,7 +2924,7 @@ class Interface: elif cursorkeys and c != -1: try: - if input == '': input = 0 + if input == '': input = 0 if floating_point: number = float(input) else: number = int(input) if c == curses.KEY_LEFT or c == ord('h'): number -= smallstep @@ -2885,6 +3011,7 @@ class Interface: 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(('Blan_k lines in non-compact', ('no','yes')[self.blank_lines])) options.append(('File _Viewer', "%s" % self.file_viewer)) @@ -2991,6 +3118,9 @@ class Interface: server.set_option('seed-queue-enabled', True) server.set_option('seed-queue-size', queue_size) + elif c == ord('k'): + self.blank_lines = not self.blank_lines + elif c == ord('v'): viewer = self.dialog_input_text('File Viewer\nExample: xdg-viewer %s', self.file_viewer) if viewer: @@ -3068,7 +3198,16 @@ def timestamp(timestamp, format="%x %X"): if timestamp < 1: return 'never' - absolute = time.strftime(format, time.localtime(timestamp)) + if timestamp > 2147483647: # Max value of 32bit signed integer (2^31-1) + # Timedelta objects do not fail on timestamps + # resulting in a date later than 2038 + date = (datetime.datetime.fromtimestamp(0) + + datetime.timedelta(seconds=timestamp)) + timeobj = date.timetuple() + else: + timeobj = time.localtime(timestamp) + + absolute = time.strftime(format, timeobj) if timestamp > time.time(): relative = 'in ' + scale_time(int(timestamp - time.time()), 'long') else: @@ -3309,7 +3448,7 @@ def save_config(filepath, force=False): def parse_sort_str(sort_str): sort_orders = [] for i in sort_str.split(','): - x = i.split(':') + x = i.split(':') if len(x) > 1: sort_orders.append( { 'name':x[1], 'reverse':True } ) else: -- cgit v1.2.3