From a8e991445ba757834bf2619f1194839ed8b37fb0 Mon Sep 17 00:00:00 2001 From: Jonathan McCrohan Date: Wed, 1 Aug 2012 19:51:47 +0100 Subject: Imported Upstream version 1.4 --- NEWS | 17 ++++ README.md | 6 +- transmission-remote-cli | 200 ++++++++++++++++++++++++++++++++---------------- 3 files changed, 156 insertions(+), 67 deletions(-) diff --git a/NEWS b/NEWS index b59b25e..1e5054d 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,20 @@ +1.4 2012-07-30 + BUGFIXES: + - Fix crash upon opening help window for small terminals + - Consider wide characters in dialogs + - Don't draw dialogs that are bigger than the terminal + - Fix file list indentation for multiple same-level directories + + - Support for Transmission 2.61 + - Highlight torrents that can't discover new peers + - Use terminal's default background/foreground colors, making + pseudo-transparency and black-on-white color schemes possible + - Use locale to find out preferred output encoding instead of forcing + UTF-8 + - Peer and tracker list are a little more compressed to fit into small + terminals + + 1.3.1 2012-06-12 BUGFIXES: - Fix wrong progress bar placement calculation for two-column diff --git a/README.md b/README.md index a923139..f4d14f9 100644 --- a/README.md +++ b/README.md @@ -78,11 +78,11 @@ torrent files with transmission-remote-cli. ## Screenshots -![Main window - full, v1.3](transmission-remote-cli/blob/master/screenshots/screenshot-mainfull-v1.3.png) +![Main window - full, v1.3](https://github.com/fagga/transmission-remote-cli/raw/master/screenshots/screenshot-mainfull-v1.3.png) -![Main window - compact, v1.3](transmission-remote-cli/blob/master/screenshots/screenshot-maincompact-v1.3.png) +![Main window - compact, v1.3](https://github.com/fagga/transmission-remote-cli/raw/master/screenshots/screenshot-maincompact-v1.3.png) -![Info window, v1.3](transmission-remote-cli/blob/master/screenshots/screenshot-details-v1.3.png) +![Info window, v1.3](https://github.com/fagga/transmission-remote-cli/raw/master/screenshots/screenshot-details-v1.3.png) ## Copyright diff --git a/transmission-remote-cli b/transmission-remote-cli index d9896e1..4bfe38d 100755 --- a/transmission-remote-cli +++ b/transmission-remote-cli @@ -16,10 +16,10 @@ # http://www.gnu.org/licenses/gpl-3.0.txt # ######################################################################## -VERSION = '1.3.1' +VERSION = '1.4' TRNSM_VERSION_MIN = '1.90' -TRNSM_VERSION_MAX = '2.52' +TRNSM_VERSION_MAX = '2.61' RPC_VERSION_MIN = 8 RPC_VERSION_MAX = 14 @@ -55,7 +55,6 @@ import os import signal import unicodedata import locale -locale.setlocale(locale.LC_ALL, '') import curses import curses.ascii from textwrap import wrap @@ -121,6 +120,7 @@ config.set('Colors', 'title_download', 'bg:blue,fg:black') config.set('Colors', 'title_idle', 'bg:cyan,fg:black') config.set('Colors', 'title_verify', 'bg:magenta,fg:black') config.set('Colors', 'title_paused', 'bg:black,fg:white') +config.set('Colors', 'title_error', 'bg:red,fg:white') config.set('Colors', 'download_rate', 'bg:black,fg:blue') config.set('Colors', 'upload_rate', 'bg:black,fg:red') config.set('Colors', 'eta+ratio', 'bg:black,fg:white') @@ -257,6 +257,7 @@ 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 @@ -392,6 +393,8 @@ 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 if response['tag'] == self.TAG_TORRENT_LIST: self.torrent_cache = response['arguments']['torrents'] @@ -723,10 +726,40 @@ class Transmission: status = 'seeding' elif torrent['status'] == Transmission.STATUS_SEED_WAIT: status = 'will seed' + elif torrent['status'] == Transmission.STATUS_ISOLATED: + status = 'isolated' else: status = 'unknown state' return status + def can_has_peers(self, torrent): + """ Will return True if at least one tracker was successfully queried + recently, or if DHT is enabled for this torrent and globally, False + otherwise. """ + + # torrent has trackers + if torrent['trackerStats']: + has_connected = any([tracker['hasAnnounced'] or tracker['hasScraped'] + for tracker in torrent['trackerStats']]) + if has_connected: + 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']: + return True + else: + # If no tracker has been queried (yet), assume at least one is online + return True + # torrent can make use of DHT + if not self.status_cache.has_key('dht-enabled') or \ + (self.status_cache['dht-enabled'] and not torrent['isPrivate']): + return True + return False + def get_bandwidth_priority(self, torrent): if torrent['bandwidthPriority'] == -1: return '-' @@ -774,6 +807,9 @@ class Interface: self.compact_torrentlist = False # draw only one line for each torrent in compact mode self.exit_now = False + locale.setlocale(locale.LC_ALL, '') + self.encoding = locale.getpreferredencoding() + self.keybindings = { ord('?'): self.call_list_key_bindings, curses.KEY_F1: self.call_list_key_bindings, @@ -864,6 +900,7 @@ class Interface: # enable colors if available try: curses.start_color() + curses.use_default_colors() self.colors = ColorManager(dict(config.items('Colors'))) for name in sorted(self.colors.get_names()): curses.init_pair(self.colors.get_id(name), @@ -892,6 +929,9 @@ class Interface: def restore_screen(self): curses.endwin() + def enc(self, text): + return text.encode(self.encoding, 'replace') + def get_screen_size(self): time.sleep(0.1) # prevents curses.error on rapid resizing while True: @@ -1110,7 +1150,8 @@ class Interface: if self.selected_torrent == -1: options = [('uploading','_Uploading'), ('downloading','_Downloading'), ('active','Ac_tive'), ('paused','_Paused'), ('seeding','_Seeding'), - ('incomplete','In_complete'), ('verifying','Verif_ying'), ('private','P_rivate'), + ('incomplete','In_complete'), ('verifying','Verif_ying'), + ('private','P_rivate'), ('isolated', '_Isolated'), ('invert','In_vert'), ('','_All')] choice = self.dialog_menu(('Show only','Filter all')[self.filter_inverse], options, map(lambda x: x[0]==self.filter_list, options).index(True)+1) @@ -1456,6 +1497,8 @@ class Interface: elif self.filter_list == 'verifying': 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] # invert list? if self.filter_inverse: self.torrents = [t for t in unfiltered if t not in self.torrents] @@ -1587,14 +1630,16 @@ class Interface: size = '| ' + size title = ljust_columns(torrent['name'], width - len(size)) + size - if torrent['status'] == Transmission.STATUS_SEED \ - or torrent['status'] == Transmission.STATUS_SEED_WAIT: + if torrent['status'] == Transmission.STATUS_SEED or \ + torrent['status'] == Transmission.STATUS_SEED_WAIT: color = curses.color_pair(self.colors.get_id('title_seed')) elif torrent['status'] == Transmission.STATUS_STOPPED: color = curses.color_pair(self.colors.get_id('title_paused')) - elif torrent['status'] == Transmission.STATUS_CHECK \ - or torrent['status'] == Transmission.STATUS_CHECK_WAIT: + elif torrent['status'] == Transmission.STATUS_CHECK or \ + torrent['status'] == Transmission.STATUS_CHECK_WAIT: color = curses.color_pair(self.colors.get_id('title_verify')) + elif torrent['status'] == Transmission.STATUS_ISOLATED: + color = curses.color_pair(self.colors.get_id('title_error')) elif torrent['rateDownload'] == 0: color = curses.color_pair(self.colors.get_id('title_idle')) elif torrent['percentDone'] < 100: @@ -1612,24 +1657,25 @@ class Interface: # addstr() dies when you tell it to draw on the last column of the # terminal, so we have to catch this exception. try: - self.pad.addstr(ypos, 0, title[0:bar_width].encode('utf-8'), tag_done) - self.pad.addstr(ypos, len_columns(title[0:bar_width]), title[bar_width:].encode('utf-8'), tag) + self.pad.addstr(ypos, 0, self.enc(title[0:bar_width]), tag_done) + self.pad.addstr(ypos, len_columns(title[0:bar_width]), self.enc(title[bar_width:]), tag) except: pass else: - self.pad.addstr(ypos, 0, title.encode('utf-8'), tag_done) + self.pad.addstr(ypos, 0, self.enc(title), tag_done) def draw_torrentlist_status(self, torrent, focused, ypos): peers = '' parts = [server.get_status(torrent)] - # show tracker error if appropriate - if torrent['errorString'] and \ - not torrent['seeders'] and not torrent['leechers'] and \ - not torrent['status'] == Transmission.STATUS_STOPPED: - parts[0] = torrent['errorString'].encode('utf-8') - + if torrent['status'] == Transmission.STATUS_ISOLATED and torrent['peersConnected'] <= 0: + if not torrent['trackerStats']: + parts[0] = "Unable to find peers without trackers and DHT disabled" + else: + tracker_errors = [tracker['lastAnnounceResult'] or tracker['lastScrapeResult'] + for tracker in torrent['trackerStats']] + parts[0] = self.enc([te for te in tracker_errors if te][0]) else: if torrent['status'] == Transmission.STATUS_CHECK: parts[0] += " (%d%%)" % int(float(torrent['recheckProgress']) * 100) @@ -1861,12 +1907,12 @@ class Interface: for i, line in enumerate(comment): if(ypos+i > self.height-1): break - self.pad.addstr(ypos+i, 50, line.encode('utf8')) + self.pad.addstr(ypos+i, 50, self.enc(line)) else: width = self.width - 2 comment = wrap_multiline(t['comment'], width, initial_indent='Comment: ') for i, line in enumerate(comment): - self.pad.addstr(ypos+6+i, 2, line.encode('utf8')) + self.pad.addstr(ypos+6+i, 2, self.enc(line)) def draw_filelist(self, ypos): column_names = ' # Progress Size Priority Filename' @@ -1891,21 +1937,21 @@ class Interface: xpos = 0 for part in re.split('(high|normal|low|off)', line[0:30], 1): if part == 'high': - self.pad.addstr(ypos, xpos, part, + self.pad.addstr(ypos, xpos, self.enc(part), curses_tags + curses.color_pair(self.colors.get_id('file_prio_high'))) elif part == 'normal': - self.pad.addstr(ypos, xpos, part, + self.pad.addstr(ypos, xpos, self.enc(part), curses_tags + curses.color_pair(self.colors.get_id('file_prio_normal'))) elif part == 'low': - self.pad.addstr(ypos, xpos, part, + self.pad.addstr(ypos, xpos, self.enc(part), curses_tags + curses.color_pair(self.colors.get_id('file_prio_low'))) elif part == 'off': - self.pad.addstr(ypos, xpos, part, + self.pad.addstr(ypos, xpos, self.enc(part), curses_tags + curses.color_pair(self.colors.get_id('file_prio_off'))) else: - self.pad.addstr(ypos, xpos, part.encode('utf-8'), curses_tags) + self.pad.addstr(ypos, xpos, self.enc(part), curses_tags) xpos += len(part) - self.pad.addstr(ypos, xpos, line[30:].encode('utf-8'), curses_tags) + self.pad.addstr(ypos, xpos, self.enc(line[30:]), curses_tags) ypos += 1 if ypos > self.height: break @@ -1939,17 +1985,36 @@ class Interface: return filelist[begin > 0 and begin or 0:] def create_filelist_transition(self, f, current_folder, filelist, current_depth, pos): - f_len = len(f) - 1 - current_folder_len = len(current_folder) + """ Create directory transition from to , + both of which are an array of strings, each one representing one + subdirectory in their path (e.g. /tmp/a/c would result in + [temp, a, c]). is a list of strings that will later be drawn + to screen. This function only creates directory strings, and is + responsible for managing depth (i.e. indentation) between different + directories. + """ + f_len = len(f) - 1 # Amount of subdirectories in f + current_folder_len = len(current_folder) # Amount of subdirectories in + # current_folder + # Number of directory parts from f and current_directory that are identical same = 0 - while same < current_folder_len and same < f_len and f[same] == current_folder[same]: + while (same < current_folder_len and + same < f_len and + f[same] == current_folder[same]): 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 f_len < current_folder_len: + + # Stepping out of a directory, but not into a new directory + if f_len < current_folder_len and f_len == same: return [current_depth, pos] + + # Increase depth for each new directory that appears in f, + # but not in current_directory while current_depth < f_len: filelist.append('%s\\ %s' % (' '*current_depth + ' '*31 , f[current_depth])) current_depth += 1 @@ -1983,12 +2048,12 @@ class Interface: if len(peer['address']) > address_width: address_width = len(peer['address']) # Column names - column_names = "Flags %3d Down %3d Up Progress ETA " % \ + column_names = 'Flags %3d Down %3d Up Progress ETA ' % \ (self.torrent_details['peersSendingToUs'], self.torrent_details['peersGettingFromUs']) - column_names += ' Client'.ljust(clientname_width + 2) \ - + " Address".ljust(address_width + 2) - if features['geoip']: column_names += " Country" - if features['dns']: column_names += " Host" + column_names += 'Client'.ljust(clientname_width + 1) \ + + 'Address'.ljust(address_width) + if features['geoip']: column_names += 'Country' + if features['dns']: column_names += ' Host' self.pad.addstr(ypos, 0, column_names.ljust(self.width), curses.A_UNDERLINE) ypos += 1 @@ -2019,7 +2084,7 @@ class Interface: # Down self.pad.addstr("%5s " % scale_bytes(peer['rateToClient']), download_tag) # Up - self.pad.addstr("%5s " % scale_bytes(peer['rateToPeer']), upload_tag) + self.pad.addstr("%5s " % scale_bytes(peer['rateToPeer']), upload_tag) # Progress if peer['progress'] < 1: self.pad.addstr("%3d%%" % (float(peer['progress'])*100)) @@ -2027,25 +2092,24 @@ class Interface: # ETA if peer['progress'] < 1 and peer['download_speed'] > 1024: - self.pad.addstr(" @ ") - self.pad.addch(curses.ACS_PLMINUS) - self.pad.addstr("%-5s " % scale_bytes(peer['download_speed'])) - self.pad.addch(curses.ACS_PLMINUS) - self.pad.addstr("%-4s " % scale_time(peer['time_left'])) + self.pad.addstr(" %6s %4s " % \ + ('~' + scale_bytes(peer['download_speed']), + '~' + scale_time(peer['time_left']))) else: - self.pad.addstr(" ") + if peer['progress'] < 1: + self.pad.addstr(" ") + else: + self.pad.addstr(" ") # Client - self.pad.addstr(peer['clientName'].ljust(clientname_width + 2).encode('utf-8')) + self.pad.addstr(self.enc(peer['clientName'].ljust(clientname_width + 1))) # Address - self.pad.addstr(peer['address'].ljust(address_width + 2)) + self.pad.addstr(peer['address'].ljust(address_width + 1)) # Country - if features['geoip']: self.pad.addstr(" %2s " % geo_ips[peer['address']]) + if features['geoip']: self.pad.addstr(" %2s " % geo_ips[peer['address']]) # Host - if features['dns']: self.pad.addstr(host_name.encode('utf-8'), curses.A_DIM) + if features['dns']: self.pad.addstr(self.enc(host_name), curses.A_DIM) ypos += 1 -#TODO -# 1. Issue #14 on GitHub is asking for feature to be able to modify trackers. def draw_trackerlist(self, ypos): top = ypos - 1 def addstr(ypos, xpos, *args): @@ -2083,7 +2147,7 @@ class Interface: addstr(ypos+i, 0, ' ', curses.A_BOLD + curses.A_REVERSE) addstr(ypos+1, 4, "Last announce: %s" % timestamp(t['lastAnnounceTime'])) - addstr(ypos+1, 57, " Last scrape: %s" % timestamp(t['lastScrapeTime'])) + addstr(ypos+1, 54, "Last scrape: %s" % timestamp(t['lastScrapeTime'])) if t['lastAnnounceSucceeded']: peers = "%s peer%s" % (num2str(t['lastAnnouncePeerCount']), ('s', '')[t['lastAnnouncePeerCount']==1]) @@ -2093,21 +2157,21 @@ class Interface: else: addstr(ypos, 2, t['announce'], curses.A_UNDERLINE) addstr(ypos+2, 9, "Response:") - announce_msg_size = self.wrap_and_draw_result(top, ypos+2, 19, t['lastAnnounceResult'].encode('utf-8')) + announce_msg_size = self.wrap_and_draw_result(top, ypos+2, 19, self.enc(t['lastAnnounceResult'])) if t['lastScrapeSucceeded']: seeds = "%s seed%s" % (num2str(t['seederCount']), ('s', '')[t['seederCount']==1]) leeches = "%s leech%s" % (num2str(t['leecherCount']), ('es', '')[t['leecherCount']==1]) - addstr(ypos+2, 57, "Tracker knows: ") - addstr(ypos+2, 72, "%s and %s" % (seeds, leeches), curses.A_BOLD) + addstr(ypos+2, 52, "Tracker knows:") + addstr(ypos+2, 67, "%s and %s" % (seeds, leeches), curses.A_BOLD) else: - addstr(ypos+2, 62, "Response:") - scrape_msg_size += self.wrap_and_draw_result(top, ypos+2, 72, t['lastScrapeResult']) + addstr(ypos+2, 57, "Response:") + scrape_msg_size += self.wrap_and_draw_result(top, ypos+2, 67, t['lastScrapeResult']) ypos += max(announce_msg_size, scrape_msg_size) addstr(ypos+3, 4, "Next announce: %s" % timestamp(t['nextAnnounceTime'])) - addstr(ypos+3, 57, " Next scrape: %s" % timestamp(t['nextScrapeTime'])) + addstr(ypos+3, 52, " Next scrape: %s" % timestamp(t['nextScrapeTime'])) ypos += 5 @@ -2158,14 +2222,14 @@ class Interface: def draw_details_list(self, ypos, info): key_width = max(map(lambda x: len(x[0]), info)) for i in info: - self.pad.addstr(ypos, 1, i[0].rjust(key_width).encode('utf-8')) # key + self.pad.addstr(ypos, 1, self.enc(i[0].rjust(key_width))) # key # value part may be wrapped if it gets too long for v in i[1:]: y, x = self.pad.getyx() if x + len(v) >= self.width: ypos += 1 self.pad.move(ypos, key_width+1) - self.pad.addstr(v.encode('utf-8')) + self.pad.addstr(self.enc(v)) ypos += 1 return ypos @@ -2326,7 +2390,7 @@ class Interface: status = "Transmission @ %s:%s" % (server.host, server.port) if cmd_args.DEBUG: status = "%d x %d " % (self.width, self.height) + status - self.screen.addstr(0, 0, status.encode('utf-8'), curses.A_REVERSE) + self.screen.addstr(0, 0, self.enc(status), curses.A_REVERSE) def draw_quick_help(self): help = [('?','Show Keybindings')] @@ -2442,23 +2506,29 @@ class Interface: ypos = 1 for line in message.split("\n"): - if len(line) > width: - line = line[0:width-7] + '...' - win.addstr(ypos, 2, line.encode('utf-8')) - ypos += 1 + if len_columns(line) > width: + line = ljust_columns(line, width-7) + '...' + + if ypos < height - 1: # ypos == height-1 is frame border + win.addstr(ypos, 2, self.enc(line)) + ypos += 1 + else: + # Do not write outside of frame border + win.addstr(ypos, 2, " More... ") + break return win def dialog_ok(self, message): height = 3 + message.count("\n") - width = max(max(map(lambda x: len(x), message.split("\n"))), 40) + 4 + width = max(max(map(lambda x: len_columns(x), message.split("\n"))), 40) + 4 win = self.window(height, width, message=message) while True: if win.getch() >= 0: return def dialog_yesno(self, message, important=False): height = 5 + message.count("\n") - width = max(len(message), 8) + 4 + width = max(len_columns(message), 8) + 4 win = self.window(height, width, message=message) win.keypad(True) @@ -2963,6 +3033,8 @@ def ljust_columns(text, max_width, padchar=' '): def len_columns(text): """ Returns the amount of columns that would occupy. """ + if type(text) == type(str()): + text = unicode(text) columns = 0 for character in text: columns += 2 if unicodedata.east_asian_width(character) in ('W', 'F') else 1 @@ -2985,7 +3057,7 @@ def debug(data): if cmd_args.DEBUG: file = open("debug.log", 'a') if type(data) == type(str()): - file.write(data.encode('utf-8')) + file.write(self.enc(data)) else: import pprint pp = pprint.PrettyPrinter(indent=4) -- cgit v1.2.3