aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--NEWS15
-rwxr-xr-xtransmission-remote-cli231
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/<number>: %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: