diff options
| author | Jonathan McCrohan <jmccrohan@gmail.com> | 2012-08-01 19:51:47 +0100 | 
|---|---|---|
| committer | Jonathan McCrohan <jmccrohan@gmail.com> | 2012-08-01 19:51:47 +0100 | 
| commit | 074d2e1f638f90a5ee28401cd386b71a5aeaa2bb (patch) | |
| tree | d3b94661f66d5d9d7b4555dc08e0999724e18b8b | |
| parent | 7620fcf293dbca9cacfedd8eb03ba5aa195934b3 (diff) | |
| parent | a8e991445ba757834bf2619f1194839ed8b37fb0 (diff) | |
| download | transmission-remote-cli-074d2e1f638f90a5ee28401cd386b71a5aeaa2bb.tar.gz | |
Merge tag 'upstream/1.4'
Upstream version 1.4
| -rw-r--r-- | NEWS | 17 | ||||
| -rw-r--r-- | README.md | 6 | ||||
| -rwxr-xr-x | transmission-remote-cli | 200 | 
3 files changed, 156 insertions, 67 deletions
| @@ -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 @@ -78,11 +78,11 @@ torrent files with transmission-remote-cli.  ## Screenshots - + - + - +  ## 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 <current_folder> to <f>, +        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]). <filelist> 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("  <guessing> ") +                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 <text> 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) | 
