ravmunken

transmission-remote-cli

Aug 10th, 2014
339
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 163.24 KB | None | 0 0
  1. #!/usr/bin/env python2
  2. ########################################################################
  3. # This is transmission-remote-cli, whereas 'cli' stands for 'Curses    #
  4. # Luminous Interface', a client for the daemon of the BitTorrent       #
  5. # client Transmission.                                                 #
  6. #                                                                      #
  7. # This program is free software: you can redistribute it and/or modify #
  8. # it under the terms of the GNU General Public License as published by #
  9. # the Free Software Foundation, either version 3 of the License, or    #
  10. # (at your option) any later version.                                  #
  11. #                                                                      #
  12. # This program is distributed in the hope that it will be useful,      #
  13. # but WITHOUT ANY WARRANTY; without even the implied warranty of       #
  14. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the        #
  15. # GNU General Public License for more details:                         #
  16. # http://www.gnu.org/licenses/gpl-3.0.txt                              #
  17. ########################################################################
  18.  
  19. VERSION = '1.7.0'
  20.  
  21. TRNSM_VERSION_MIN = '1.90'
  22. TRNSM_VERSION_MAX = '2.82'
  23. RPC_VERSION_MIN = 8
  24. RPC_VERSION_MAX = 15
  25.  
  26. # error codes
  27. CONNECTION_ERROR = 1
  28. JSON_ERROR       = 2
  29. CONFIGFILE_ERROR = 3
  30.  
  31. # use simplejson if available because it seems to be faster
  32. try:
  33.     import simplejson as json
  34. except ImportError:
  35.     try:
  36.         # Python 2.6 comes with a json module ...
  37.         import json
  38.         # ...but there is also an old json module that doesn't support .loads/.dumps.
  39.         json.dumps ; json.dumps
  40.     except (ImportError,AttributeError):
  41.         quit("Please install simplejson or Python 2.6 or higher.")
  42.  
  43. import time
  44. import datetime
  45. import re
  46. import base64
  47. import httplib
  48. import urllib2
  49. import socket
  50. socket.setdefaulttimeout(None)
  51. import ConfigParser
  52. from optparse import OptionParser, SUPPRESS_HELP
  53. import sys
  54. import os
  55. import signal
  56. import unicodedata
  57. import locale
  58. import curses
  59. import curses.ascii
  60. from textwrap import wrap
  61. from subprocess import call, Popen
  62. import netrc
  63. import operator
  64. import urlparse
  65. from distutils.spawn import find_executable
  66.  
  67. locale.setlocale(locale.LC_ALL, '')
  68. ENCODING = locale.getpreferredencoding() or 'UTF-8'
  69.  
  70. # optional features provided by non-standard modules
  71. features = {'dns':False, 'geoip':False, 'ipy':False}
  72. try:   import adns; features['dns'] = True     # resolve IP to host name
  73. except ImportError: features['dns'] = False
  74.  
  75. try:   import GeoIP; features['geoip'] = True  # show country peer seems to be in
  76. except ImportError:  features['geoip'] = False
  77.  
  78. try:   import IPy;  features['ipy'] = True  # extract ipv4 from ipv6 addresses
  79. except ImportError: features['ipy'] = False
  80.  
  81.  
  82. if features['ipy']:
  83.     IPV6_RANGE_6TO4 = IPy.IP('2002::/16')
  84.     IPV6_RANGE_TEREDO = IPy.IP('2001::/32')
  85.     IPV4_ONES = 0xffffffff
  86.  
  87. if features['geoip']:
  88.     def country_code_by_addr_vany(geo_ip, geo_ip6, addr):
  89.         if '.' in addr:
  90.             return geo_ip.country_code_by_addr(addr)
  91.         if not ':' in addr:
  92.             return None
  93.         if features['ipy']:
  94.             ip = IPy.IP(addr)
  95.             if ip in IPV6_RANGE_6TO4:
  96.               addr = str(IPy.IP(ip.int() >> 80 & IPV4_ONES))
  97.               return geo_ip.country_code_by_addr(addr)
  98.             elif ip in IPV6_RANGE_TEREDO:
  99.               addr = str(IPy.IP(ip.int() & IPV4_ONES ^ IPV4_ONES))
  100.               return geo_ip.country_code_by_addr(addr)
  101.         if hasattr(geo_ip6, 'country_code_by_addr_v6'):
  102.             return geo_ip6.country_code_by_addr_v6(addr)
  103.  
  104.  
  105. # define config defaults
  106. config = ConfigParser.SafeConfigParser()
  107. config.add_section('Connection')
  108. config.set('Connection', 'password', '')
  109. config.set('Connection', 'username', '')
  110. config.set('Connection', 'port', '9091')
  111. config.set('Connection', 'host', 'localhost')
  112. config.set('Connection', 'path', '/transmission/rpc')
  113. config.set('Connection', 'ssl', 'False')
  114. config.add_section('Sorting')
  115. config.set('Sorting', 'order', 'name')
  116. config.add_section('Filtering')
  117. config.set('Filtering', 'filter', '')
  118. config.set('Filtering', 'invert', 'False')
  119. config.add_section('Misc')
  120. config.set('Misc', 'compact_list', 'False')
  121. config.set('Misc', 'blank_lines', 'True')
  122. config.set('Misc', 'torrentname_is_progressbar', 'True')
  123. config.set('Misc', 'file_viewer', 'xdg-open %%s')
  124. config.set('Misc', 'file_open_in_terminal', 'True')
  125. config.add_section('Colors')
  126. config.set('Colors', 'title_seed',       'bg:green,fg:background')
  127. config.set('Colors', 'title_download',   'bg:blue,fg:background')
  128. config.set('Colors', 'title_idle',       'bg:cyan,fg:background')
  129. config.set('Colors', 'title_verify',     'bg:magenta,fg:background')
  130. config.set('Colors', 'title_paused',     'bg:background,fg:white')
  131. config.set('Colors', 'title_error',      'bg:red,fg:white')
  132. config.set('Colors', 'download_rate',    'bg:background,fg:blue')
  133. config.set('Colors', 'upload_rate',      'bg:bacground,fg:red')
  134. config.set('Colors', 'eta+ratio',        'bg:background,fg:white')
  135. config.set('Colors', 'filter_status',    'bg:red,fg:background')
  136. config.set('Colors', 'dialog',           'bg:background,fg:white')
  137. config.set('Colors', 'dialog_important', 'bg:red,fg:background')
  138. config.set('Colors', 'button',           'bg:white,fg:background')
  139. config.set('Colors', 'button_focused',   'bg:background,fg:white')
  140. config.set('Colors', 'file_prio_high',   'bg:red,fg:background')
  141. config.set('Colors', 'file_prio_normal', 'bg:white,fg:background')
  142. config.set('Colors', 'file_prio_low',    'bg:yellow,fg:background')
  143. config.set('Colors', 'file_prio_off',    'bg:blue,fg:background')
  144.  
  145.  
  146. class ColorManager:
  147.     def __init__(self, config):
  148.         self.config = dict()
  149.         self.term_has_colors = curses.has_colors()
  150.         curses.start_color()
  151.         if self.term_has_colors:
  152.             curses.use_default_colors()
  153.         for name in config.keys():
  154.             self.config[name] = self._parse_color_pair(config[name])
  155.             if self.term_has_colors:
  156.                 curses.init_pair(self.config[name]['id'],
  157.                                  self.config[name]['fg'],
  158.                                  self.config[name]['bg'])
  159.  
  160.     def _parse_color_pair(self, pair):
  161.         # BG and FG are intentionally switched here because colors are always
  162.         # used with curses.A_REVERSE. (To be honest, I forgot why, probably
  163.         # has something to do with how highlighting focus works.)
  164.         bg_name = pair.split(',')[1].split(':')[1].upper()
  165.         fg_name = pair.split(',')[0].split(':')[1].upper()
  166.         color_pair = { 'id': len(self.config.keys()) + 1 }
  167.         try:
  168.             color_pair['bg'] = eval('curses.COLOR_' + bg_name)
  169.         except AttributeError:
  170.             color_pair['bg'] = -1
  171.         try:
  172.             color_pair['fg'] = eval('curses.COLOR_' + fg_name)
  173.         except AttributeError:
  174.             color_pair['fg'] = -0
  175.         return color_pair
  176.  
  177.     def id(self, name): return self.config[name]['id']
  178.  
  179.  
  180. class Normalizer:
  181.     def __init__(self):
  182.         self.values = {}
  183.  
  184.     def add(self, id, value, max):
  185.         if not id in self.values.keys():
  186.             self.values[id] = [ float(value) ]
  187.         else:
  188.             if len(self.values[id]) >= max:
  189.                 self.values[id].pop(0)
  190.             self.values[id].append(float(value))
  191.         return self.get(id)
  192.  
  193.     def get(self, id):
  194.         if not id in self.values.keys():
  195.             return 0.0
  196.         return sum(self.values[id]) / len(self.values[id])
  197.  
  198.  
  199.  
  200. authhandler = None
  201. session_id = 0
  202. vmode_id = -1
  203.  
  204. # Handle communication with Transmission server.
  205. class TransmissionRequest:
  206.     def __init__(self, host, port, path, method=None, tag=None, arguments=None):
  207.         self.url           = create_url(host, port, path)
  208.         self.open_request  = None
  209.         self.last_update   = 0
  210.         if method and tag:
  211.             self.set_request_data(method, tag, arguments)
  212.  
  213.     def set_request_data(self, method, tag, arguments=None):
  214.         request_data = {'method':method, 'tag':tag}
  215.         if arguments: request_data['arguments'] = arguments
  216.         self.http_request = urllib2.Request(url=self.url, data=json.dumps(request_data))
  217.  
  218.     def send_request(self):
  219.         """Ask for information from server OR submit command."""
  220.  
  221.         global session_id
  222.         try:
  223.             if session_id:
  224.                 self.http_request.add_header('X-Transmission-Session-Id', session_id)
  225.             self.open_request = urllib2.urlopen(self.http_request)
  226.         except AttributeError:
  227.             # request data (http_request) isn't specified yet -- data will be available on next call
  228.             pass
  229.  
  230.         # authentication
  231.         except urllib2.HTTPError, e:
  232.             try:
  233.                 msg = html2text(str(e.read()))
  234.             except:
  235.                 msg = str(e)
  236.  
  237.             # extract session id and send request again
  238.             m = re.search('X-Transmission-Session-Id:\s*(\w+)', msg)
  239.             try:
  240.                 session_id = m.group(1)
  241.                 self.send_request()
  242.             except AttributeError:
  243.                 quit(str(msg) + "\n", CONNECTION_ERROR)
  244.  
  245.         except urllib2.URLError, msg:
  246.             try:
  247.                 reason = msg.reason[1]
  248.             except IndexError:
  249.                 reason = str(msg.reason)
  250.             quit("Cannot connect to %s: %s\n" % (self.http_request.host, reason), CONNECTION_ERROR)
  251.  
  252.     def get_response(self):
  253.         """Get response to previously sent request."""
  254.  
  255.         if self.open_request == None:
  256.             return {'result': 'no open request'}
  257.         response = self.open_request.read()
  258.         # work around regression in Python 2.6.5, caused by http://bugs.python.org/issue8797
  259.         if authhandler:
  260.             authhandler.retried = 0
  261.         try:
  262.             data = json.loads(unicode(response))
  263.         except ValueError:
  264.             quit("Cannot parse response: %s\n" % response, JSON_ERROR)
  265.         self.open_request = None
  266.         return data
  267.  
  268.  
  269. # End of Class TransmissionRequest
  270.  
  271.  
  272. # Higher level of data exchange
  273. class Transmission:
  274.     STATUS_STOPPED       = 0   # Torrent is stopped
  275.     STATUS_CHECK_WAIT    = 1   # Queued to check files
  276.     STATUS_CHECK         = 2   # Checking files
  277.     STATUS_DOWNLOAD_WAIT = 3   # Queued to download
  278.     STATUS_DOWNLOAD      = 4   # Downloading
  279.     STATUS_SEED_WAIT     = 5   # Queued to seed
  280.     STATUS_SEED          = 6   # Seeding
  281.  
  282.     TAG_TORRENT_LIST    = 7
  283.     TAG_TORRENT_DETAILS = 77
  284.     TAG_SESSION_STATS   = 21
  285.     TAG_SESSION_GET     = 22
  286.  
  287.     LIST_FIELDS = [ 'id', 'name', 'downloadDir', 'status', 'trackerStats', 'desiredAvailable',
  288.                     'rateDownload', 'rateUpload', 'eta', 'uploadRatio',
  289.                     'sizeWhenDone', 'haveValid', 'haveUnchecked', 'addedDate',
  290.                     'uploadedEver', 'errorString', 'recheckProgress',
  291.                     'peersConnected', 'uploadLimit', 'downloadLimit',
  292.                     'uploadLimited', 'downloadLimited', 'bandwidthPriority',
  293.                     'peersSendingToUs', 'peersGettingFromUs',
  294.                     'seedRatioLimit', 'seedRatioMode', 'isPrivate' ]
  295.  
  296.     DETAIL_FIELDS = [ 'files', 'priorities', 'wanted', 'peers', 'trackers',
  297.                       'activityDate', 'dateCreated', 'startDate', 'doneDate',
  298.                       'totalSize', 'leftUntilDone', 'comment', 'creator',
  299.                       'hashString', 'pieceCount', 'pieceSize', 'pieces',
  300.                       'downloadedEver', 'corruptEver', 'peersFrom' ] + LIST_FIELDS
  301.  
  302.     def __init__(self, host, port, path, username, password):
  303.         self.host = host
  304.         self.port = port
  305.         self.path = path
  306.  
  307.         if username and password:
  308.             password_mgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
  309.             password_mgr.add_password(None, create_url(host, port, path), username, password)
  310.             global authhandler
  311.             authhandler = urllib2.HTTPBasicAuthHandler(password_mgr)
  312.             opener = urllib2.build_opener(authhandler)
  313.             urllib2.install_opener(opener)
  314.  
  315.         # check rpc version
  316.         request = TransmissionRequest(host, port, path, 'session-get', self.TAG_SESSION_GET)
  317.         request.send_request()
  318.         response = request.get_response()
  319.  
  320.         self.rpc_version = response['arguments']['rpc-version']
  321.         self.version = response['arguments']['version'].split()[0]
  322.  
  323.         # rpc version too old?
  324.         version_error = "Unsupported Transmission version: " + str(response['arguments']['version']) + \
  325.             " -- RPC protocol version: " + str(response['arguments']['rpc-version']) + "\n"
  326.  
  327.         min_msg = "Please install Transmission version " + TRNSM_VERSION_MIN + " or higher.\n"
  328.         try:
  329.             if response['arguments']['rpc-version'] < RPC_VERSION_MIN:
  330.                 quit(version_error + min_msg)
  331.         except KeyError:
  332.             quit(version_error + min_msg)
  333.  
  334.         # rpc version too new?
  335.         if response['arguments']['rpc-version'] > RPC_VERSION_MAX:
  336.             quit(version_error + "Please install Transmission version " + TRNSM_VERSION_MAX + " or lower.\n")
  337.  
  338.         # setup compatibility to Transmission <2.40
  339.         if self.rpc_version < 14:
  340.             Transmission.STATUS_CHECK_WAIT    = 1 << 0
  341.             Transmission.STATUS_CHECK         = 1 << 1
  342.             Transmission.STATUS_DOWNLOAD_WAIT = 1 << 2
  343.             Transmission.STATUS_DOWNLOAD      = 1 << 2
  344.             Transmission.STATUS_SEED_WAIT     = 1 << 3
  345.             Transmission.STATUS_SEED          = 1 << 3
  346.             Transmission.STATUS_STOPPED       = 1 << 4
  347.  
  348.         # Queue was implemented in Transmission v2.4
  349.         if self.rpc_version >= 14:
  350.             self.LIST_FIELDS.append('queuePosition');
  351.             self.DETAIL_FIELDS.append('queuePosition');
  352.  
  353.         # set up request list
  354.         self.requests = {'torrent-list':
  355.                              TransmissionRequest(host, port, path, 'torrent-get', self.TAG_TORRENT_LIST, {'fields': self.LIST_FIELDS}),
  356.                          'session-stats':
  357.                              TransmissionRequest(host, port, path, 'session-stats', self.TAG_SESSION_STATS, 21),
  358.                          'session-get':
  359.                              TransmissionRequest(host, port, path, 'session-get', self.TAG_SESSION_GET),
  360.                          'torrent-details':
  361.                              TransmissionRequest(host, port, path)}
  362.  
  363.         self.torrent_cache = []
  364.         self.status_cache  = dict()
  365.         self.torrent_details_cache = dict()
  366.         self.peer_progress_cache   = dict()
  367.         self.hosts_cache   = dict()
  368.         self.geo_ips_cache = dict()
  369.         if features['dns']:   self.resolver = adns.init()
  370.         if features['geoip']:
  371.             self.geo_ip = GeoIP.new(GeoIP.GEOIP_MEMORY_CACHE)
  372.             try:
  373.                 self.geo_ip6 = GeoIP.open_type(GeoIP.GEOIP_COUNTRY_EDITION_V6, GeoIP.GEOIP_MEMORY_CACHE);
  374.             except AttributeError: self.geo_ip6 = None
  375.             except GeoIP.error: self.geo_ip6 = None
  376.  
  377.         # make sure there are no undefined values
  378.         self.wait_for_torrentlist_update()
  379.         self.requests['torrent-details'] = TransmissionRequest(self.host, self.port, self.path)
  380.  
  381.  
  382.     def update(self, delay, tag_waiting_for=0):
  383.         """Maintain up-to-date data."""
  384.  
  385.         tag_waiting_for_occurred = False
  386.  
  387.         for request in self.requests.values():
  388.             if time.time() - request.last_update >= delay:
  389.                 request.last_update = time.time()
  390.                 response = request.get_response()
  391.  
  392.                 if response['result'] == 'no open request':
  393.                     request.send_request()
  394.  
  395.                 elif response['result'] == 'success':
  396.                     tag = self.parse_response(response)
  397.                     if tag == tag_waiting_for:
  398.                         tag_waiting_for_occurred = True
  399.  
  400.         if tag_waiting_for:
  401.             return tag_waiting_for_occurred
  402.         else:
  403.             return None
  404.  
  405.  
  406.  
  407.     def parse_response(self, response):
  408.         def get_main_tracker_domain(torrent):
  409.             if torrent['trackerStats']:
  410.                 trackers = sorted(torrent['trackerStats'],
  411.                                   key=operator.itemgetter('tier', 'id'))
  412.                 return urlparse.urlparse(trackers[0]['announce']).hostname
  413.             else:
  414.                 # Trackerless torrents
  415.                 return None
  416.  
  417.         # response is a reply to torrent-get
  418.         if response['tag'] == self.TAG_TORRENT_LIST or response['tag'] == self.TAG_TORRENT_DETAILS:
  419.             for t in response['arguments']['torrents']:
  420.                 t['uploadRatio'] = round(float(t['uploadRatio']), 2)
  421.                 t['percentDone'] = percent(float(t['sizeWhenDone']),
  422.                                            float(t['haveValid'] + t['haveUnchecked']))
  423.                 t['available'] = t['desiredAvailable'] + t['haveValid'] + t['haveUnchecked']
  424.                 if t['downloadDir'][-1] != '/':
  425.                     t['downloadDir'] += '/'
  426.                 try:
  427.                     t['seeders']  = max(map(lambda x: x['seederCount'],  t['trackerStats']))
  428.                     t['leechers'] = max(map(lambda x: x['leecherCount'], t['trackerStats']))
  429.                 except ValueError:
  430.                     t['seeders']  = t['leechers'] = -1
  431.                 t['isIsolated'] = not self.can_has_peers(t)
  432.                 t['mainTrackerDomain'] = get_main_tracker_domain(t)
  433.  
  434.             if response['tag'] == self.TAG_TORRENT_LIST:
  435.                 self.torrent_cache = response['arguments']['torrents']
  436.  
  437.             elif response['tag'] == self.TAG_TORRENT_DETAILS:
  438.                 # torrent list may be empty sometimes after deleting
  439.                 # torrents.  no idea why and why the server sends us
  440.                 # TAG_TORRENT_DETAILS, but just passing seems to help.(?)
  441.                 try:
  442.                     torrent_details = response['arguments']['torrents'][0]
  443.                     torrent_details['pieces'] = base64.decodestring(torrent_details['pieces'])
  444.                     self.torrent_details_cache = torrent_details
  445.                     self.upgrade_peerlist()
  446.                 except IndexError:
  447.                     pass
  448.  
  449.         elif response['tag'] == self.TAG_SESSION_STATS:
  450.             self.status_cache.update(response['arguments'])
  451.  
  452.         elif response['tag'] == self.TAG_SESSION_GET:
  453.             self.status_cache.update(response['arguments'])
  454.  
  455.         return response['tag']
  456.  
  457.     def upgrade_peerlist(self):
  458.         for index,peer in enumerate(self.torrent_details_cache['peers']):
  459.             ip = peer['address']
  460.             peerid = ip + self.torrent_details_cache['hashString']
  461.  
  462.             # make sure peer cache exists
  463.             if not self.peer_progress_cache.has_key(peerid):
  464.                 self.peer_progress_cache[peerid] = {'last_progress':peer['progress'], 'last_update':time.time(),
  465.                                                     'download_speed':0, 'time_left':0}
  466.  
  467.             this_peer = self.peer_progress_cache[peerid]
  468.             this_torrent = self.torrent_details_cache
  469.  
  470.             # estimate how fast a peer is downloading
  471.             if peer['progress'] < 1:
  472.                 this_time = time.time()
  473.                 time_diff = this_time - this_peer['last_update']
  474.                 progress_diff = peer['progress'] - this_peer['last_progress']
  475.                 if this_peer['last_progress'] and progress_diff > 0 and time_diff > 5:
  476.                     download_left = this_torrent['totalSize'] - \
  477.                         (this_torrent['totalSize']*peer['progress'])
  478.                     downloaded = this_torrent['totalSize'] * progress_diff
  479.  
  480.                     this_peer['download_speed'] = \
  481.                         norm.add(peerid+':download_speed', downloaded/time_diff, 10)
  482.                     this_peer['time_left']   = download_left/this_peer['download_speed']
  483.                     this_peer['last_update'] = this_time
  484.  
  485.                 # infrequent progress updates lead to increasingly inaccurate
  486.                 # estimates, so we go back to <guessing>
  487.                 elif time_diff > 60:
  488.                     this_peer['download_speed'] = 0
  489.                     this_peer['time_left']      = 0
  490.                     this_peer['last_update']    = time.time()
  491.                 this_peer['last_progress'] = peer['progress']  # remember progress
  492.             this_torrent['peers'][index].update(this_peer)
  493.  
  494.             # resolve and locate peer's ip
  495.             if features['dns'] and not self.hosts_cache.has_key(ip):
  496.                 try:
  497.                     self.hosts_cache[ip] = self.resolver.submit_reverse(ip, adns.rr.PTR)
  498.                 except adns.Error:
  499.                     pass
  500.             if features['geoip'] and not self.geo_ips_cache.has_key(ip):
  501.                 self.geo_ips_cache[ip] = country_code_by_addr_vany(self.geo_ip, self.geo_ip6, ip)
  502.                 if self.geo_ips_cache[ip] == None:
  503.                     self.geo_ips_cache[ip] = '?'
  504.  
  505.     def get_rpc_version(self):
  506.         return self.rpc_version
  507.  
  508.     def get_global_stats(self):
  509.         return self.status_cache
  510.  
  511.     def get_torrent_list(self, sort_orders):
  512.         def sort_value(value):
  513.             try:
  514.                 return value.lower()
  515.             except AttributeError:
  516.                 return value
  517.         try:
  518.             for sort_order in sort_orders:
  519.                 self.torrent_cache.sort(key=lambda x: sort_value(x[sort_order['name']]),
  520.                                         reverse=sort_order['reverse'])
  521.         except IndexError:
  522.             return []
  523.         return self.torrent_cache
  524.  
  525.     def get_torrent_by_id(self, id):
  526.         i = 0
  527.         while self.torrent_cache[i]['id'] != id:  i += 1
  528.         if self.torrent_cache[i]['id'] == id:
  529.             return self.torrent_cache[i]
  530.         else:
  531.             return None
  532.  
  533.  
  534.     def get_torrent_details(self):
  535.         return self.torrent_details_cache
  536.     def set_torrent_details_id(self, id):
  537.         if id < 0:
  538.             self.requests['torrent-details'] = TransmissionRequest(self.host, self.port, self.path)
  539.         else:
  540.             self.requests['torrent-details'].set_request_data('torrent-get', self.TAG_TORRENT_DETAILS,
  541.                                                               {'ids':id, 'fields': self.DETAIL_FIELDS})
  542.  
  543.     def get_hosts(self):
  544.         return self.hosts_cache
  545.  
  546.     def get_geo_ips(self):
  547.         return self.geo_ips_cache
  548.  
  549.  
  550.     def set_option(self, option_name, option_value):
  551.         request = TransmissionRequest(self.host, self.port, self.path, 'session-set', 1, {option_name: option_value})
  552.         request.send_request()
  553.         self.wait_for_status_update()
  554.  
  555.  
  556.     def set_rate_limit(self, direction, new_limit, torrent_id=-1):
  557.         data = dict()
  558.         if new_limit <= -1:
  559.             new_limit     = None
  560.             limit_enabled = False
  561.         else:
  562.             limit_enabled = True
  563.  
  564.         if torrent_id < 0:
  565.             type = 'session-set'
  566.             data['speed-limit-'+direction]            = new_limit
  567.             data['speed-limit-'+direction+'-enabled'] = limit_enabled
  568.         else:
  569.             type = 'torrent-set'
  570.             data['ids'] = [torrent_id]
  571.             data[direction+'loadLimit']   = new_limit
  572.             data[direction+'loadLimited'] = limit_enabled
  573.  
  574.         request = TransmissionRequest(self.host, self.port, self.path, type, 1, data)
  575.         request.send_request()
  576.         self.wait_for_torrentlist_update()
  577.  
  578.  
  579.     def set_seed_ratio(self, ratio, torrent_id=-1):
  580.         data = dict()
  581.         if ratio == -1:
  582.             ratio = None
  583.             mode  = 0   # Use global settings
  584.         elif ratio == 0:
  585.             ratio = None
  586.             mode  = 2   # Seed regardless of ratio
  587.         elif ratio >= 0:
  588.             mode  = 1   # Stop seeding at seedRatioLimit
  589.         else:
  590.             return
  591.  
  592.         data['ids']            = [torrent_id]
  593.         data['seedRatioLimit'] = ratio
  594.         data['seedRatioMode']  = mode
  595.         request = TransmissionRequest(self.host, self.port, self.path, 'torrent-set', 1, data)
  596.         request.send_request()
  597.         self.wait_for_torrentlist_update()
  598.  
  599.  
  600.     def increase_bandwidth_priority(self, torrent_id):
  601.         torrent = self.get_torrent_by_id(torrent_id)
  602.         if torrent == None or torrent['bandwidthPriority'] >= 1:
  603.             return False
  604.         else:
  605.             new_priority = torrent['bandwidthPriority'] + 1
  606.             request = TransmissionRequest(self.host, self.port, self.path, 'torrent-set', 1,
  607.                                           {'ids': [torrent_id], 'bandwidthPriority':new_priority})
  608.             request.send_request()
  609.             self.wait_for_torrentlist_update()
  610.  
  611.     def decrease_bandwidth_priority(self, torrent_id):
  612.         torrent = self.get_torrent_by_id(torrent_id)
  613.         if torrent == None or torrent['bandwidthPriority'] <= -1:
  614.             return False
  615.         else:
  616.             new_priority = torrent['bandwidthPriority'] - 1
  617.             request = TransmissionRequest(self.host, self.port, self.path, 'torrent-set', 1,
  618.                                           {'ids': [torrent_id], 'bandwidthPriority':new_priority})
  619.             request.send_request()
  620.             self.wait_for_torrentlist_update()
  621.  
  622.     def move_queue(self, torrent_id, new_position):
  623.         args = {'ids': [ torrent_id ] }
  624.         if new_position in ('up', 'down', 'top', 'bottom'):
  625.             method_name = 'queue-move-' + new_position
  626.         elif isinstance(new_position, int):
  627.             method_name = 'torrent-set'
  628.             args['queuePosition'] = min(max(new_position, 0), len(self.torrent_cache)-1)
  629.         else:
  630.             raise ValueError("Is not up/down/top/bottom/<number>: %s" % new_position)
  631.  
  632.         request = TransmissionRequest(self.host, self.port, self.path, method_name, 1, args)
  633.         request.send_request()
  634.         self.wait_for_torrentlist_update()
  635.  
  636.     def toggle_turtle_mode(self):
  637.         self.set_option('alt-speed-enabled', not self.status_cache['alt-speed-enabled'])
  638.  
  639.  
  640.     def add_torrent(self, location):
  641.         args = {}
  642.         try:
  643.             with  open(location, 'rb') as fp:
  644.                 args['metainfo'] = unicode(base64.b64encode(fp.read()))
  645.         # If the file doesnt exist or we cant open it, then it is either a url or needs to
  646.         # be open by the server
  647.         except IOError:
  648.             args['filename'] = location
  649.  
  650.         request = TransmissionRequest(self.host, self.port, self.path, 'torrent-add', 1, args)
  651.         request.send_request()
  652.         response = request.get_response()
  653.         if response['result'] != 'success':
  654.             return response['result']
  655.         else:
  656.             return ''
  657.  
  658.     def stop_torrents(self, ids):
  659.         request = TransmissionRequest(self.host, self.port, self.path, 'torrent-stop', 1, {'ids': ids})
  660.         request.send_request()
  661.         self.wait_for_torrentlist_update()
  662.  
  663.     def start_torrents(self, ids):
  664.         request = TransmissionRequest(self.host, self.port, self.path, 'torrent-start', 1, {'ids': ids})
  665.         request.send_request()
  666.         self.wait_for_torrentlist_update()
  667.  
  668.     def start_now_torrent(self, id):
  669.         request = TransmissionRequest(self.host, self.port, self.path, 'torrent-start-now', 1, {'ids': [id]})
  670.         request.send_request()
  671.         self.wait_for_torrentlist_update()
  672.  
  673.     def verify_torrent(self, id):
  674.         request = TransmissionRequest(self.host, self.port, self.path, 'torrent-verify', 1, {'ids': [id]})
  675.         request.send_request()
  676.         self.wait_for_torrentlist_update()
  677.  
  678.     def reannounce_torrent(self, id):
  679.         request = TransmissionRequest(self.host, self.port, self.path, 'torrent-reannounce', 1, {'ids': [id]})
  680.         request.send_request()
  681.         self.wait_for_torrentlist_update()
  682.  
  683.     def move_torrent(self, torrent_id, new_location):
  684.         request = TransmissionRequest(self.host, self.port, self.path, 'torrent-set-location', 1,
  685.                                       {'ids': torrent_id, 'location': new_location, 'move': True})
  686.         request.send_request()
  687.         self.wait_for_torrentlist_update()
  688.  
  689.     def remove_torrent(self, id):
  690.         request = TransmissionRequest(self.host, self.port, self.path, 'torrent-remove', 1, {'ids': [id]})
  691.         request.send_request()
  692.         self.wait_for_torrentlist_update()
  693.  
  694.     def remove_torrent_local_data(self, id):
  695.         request = TransmissionRequest(self.host, self.port, self.path, 'torrent-remove', 1, {'ids': [id], 'delete-local-data':True})
  696.         request.send_request()
  697.         self.wait_for_torrentlist_update()
  698.  
  699.     def add_torrent_tracker(self, id, tracker):
  700.         data = { 'ids' : [id],
  701.                  'trackerAdd' : [tracker] }
  702.         request = TransmissionRequest(self.host, self.port, self.path, 'torrent-set', 1, data)
  703.         request.send_request()
  704.         response = request.get_response()
  705.         return response['result'] if response['result'] != 'success' else ''
  706.  
  707.     def remove_torrent_tracker(self, id, tracker):
  708.         data = { 'ids' : [id],
  709.                  'trackerRemove' : [tracker] }
  710.         request = TransmissionRequest(self.host, self.port, self.path, 'torrent-set', 1, data)
  711.         request.send_request()
  712.         response = request.get_response()
  713.         self.wait_for_torrentlist_update()
  714.         return response['result'] if response['result'] != 'success' else ''
  715.  
  716.     def increase_file_priority(self, file_nums):
  717.         file_nums = list(file_nums)
  718.         ref_num = file_nums[0]
  719.         for num in file_nums:
  720.             if not self.torrent_details_cache['wanted'][num]:
  721.                 ref_num = num
  722.                 break
  723.             elif self.torrent_details_cache['priorities'][num] < \
  724.                     self.torrent_details_cache['priorities'][ref_num]:
  725.                 ref_num = num
  726.         current_priority = self.torrent_details_cache['priorities'][ref_num]
  727.         if not self.torrent_details_cache['wanted'][ref_num]:
  728.             self.set_file_priority(self.torrent_details_cache['id'], file_nums, 'low')
  729.         elif current_priority == -1:
  730.             self.set_file_priority(self.torrent_details_cache['id'], file_nums, 'normal')
  731.         elif current_priority == 0:
  732.             self.set_file_priority(self.torrent_details_cache['id'], file_nums, 'high')
  733.  
  734.     def decrease_file_priority(self, file_nums):
  735.         file_nums = list(file_nums)
  736.         ref_num = file_nums[0]
  737.         for num in file_nums:
  738.             if self.torrent_details_cache['priorities'][num] > \
  739.                     self.torrent_details_cache['priorities'][ref_num]:
  740.                 ref_num = num
  741.         current_priority = self.torrent_details_cache['priorities'][ref_num]
  742.         if current_priority >= 1:
  743.             self.set_file_priority(self.torrent_details_cache['id'], file_nums, 'normal')
  744.         elif current_priority == 0:
  745.             self.set_file_priority(self.torrent_details_cache['id'], file_nums, 'low')
  746.         elif current_priority == -1:
  747.             self.set_file_priority(self.torrent_details_cache['id'], file_nums, 'off')
  748.  
  749.  
  750.     def set_file_priority(self, torrent_id, file_nums, priority):
  751.         request_data = {'ids': [torrent_id]}
  752.         if priority == 'off':
  753.             request_data['files-unwanted'] = file_nums
  754.         else:
  755.             request_data['files-wanted'] = file_nums
  756.             request_data['priority-' + priority] = file_nums
  757.         request = TransmissionRequest(self.host, self.port, self.path, 'torrent-set', 1, request_data)
  758.         request.send_request()
  759.         self.wait_for_details_update()
  760.  
  761.     def get_file_priority(self, torrent_id, file_num):
  762.         priority = self.torrent_details_cache['priorities'][file_num]
  763.         if not self.torrent_details_cache['wanted'][file_num]: return 'off'
  764.         elif priority <= -1: return 'low'
  765.         elif priority == 0:  return 'normal'
  766.         elif priority >= 1:  return 'high'
  767.         return '?'
  768.  
  769.     def wait_for_torrentlist_update(self):
  770.         self.wait_for_update(7)
  771.     def wait_for_details_update(self):
  772.         self.wait_for_update(77)
  773.     def wait_for_status_update(self):
  774.         self.wait_for_update(22)
  775.     def wait_for_update(self, update_id):
  776.         self.update(0) # send request
  777.         while True:    # wait for response
  778.             if self.update(0, update_id): break
  779.             time.sleep(0.1)
  780.  
  781.     def get_status(self, torrent):
  782.         if torrent['status'] == Transmission.STATUS_STOPPED:
  783.             status = 'paused'
  784.         elif torrent['status'] == Transmission.STATUS_CHECK:
  785.             status = 'verifying'
  786.         elif torrent['status'] == Transmission.STATUS_CHECK_WAIT:
  787.             status = 'will verify'
  788.         elif torrent['isIsolated']:
  789.             status = 'isolated'
  790.         elif torrent['status'] == Transmission.STATUS_DOWNLOAD:
  791.             status = ('idle','downloading')[torrent['rateDownload'] > 0]
  792.         elif torrent['status'] == Transmission.STATUS_DOWNLOAD_WAIT:
  793.             status = 'will download (%d)' % torrent['queuePosition']
  794.         elif torrent['status'] == Transmission.STATUS_SEED:
  795.             status = 'seeding'
  796.         elif torrent['status'] == Transmission.STATUS_SEED_WAIT:
  797.             status = 'will seed (%d)' % torrent['queuePosition']
  798.         else:
  799.             status = 'unknown state'
  800.         return status
  801.  
  802.     def can_has_peers(self, torrent):
  803.         """ Will return True if at least one tracker was successfully queried
  804.        recently, or if DHT is enabled for this torrent and globally, False
  805.        otherwise. """
  806.  
  807.         # Torrent has trackers?
  808.         if torrent['trackerStats']:
  809.             # Did we try to connect a tracker?
  810.             if any([tracker['hasAnnounced'] for tracker in torrent['trackerStats']]):
  811.                 for tracker in torrent['trackerStats']:
  812.                     if tracker['lastAnnounceSucceeded']:
  813.                         return True
  814.             # We didn't try yet; assume at least one is online
  815.             else:
  816.                 return True
  817.         # Torrent can use DHT?
  818.         # ('dht-enabled' may be missing; assume DHT is available until we can say for sure)
  819.         if not self.status_cache.has_key('dht-enabled') or \
  820.                 (self.status_cache['dht-enabled'] and not torrent['isPrivate']):
  821.             return True
  822.         # No ways of finding peers remaining
  823.         return False
  824.  
  825.     def get_bandwidth_priority(self, torrent):
  826.         if torrent['bandwidthPriority'] == -1:
  827.             return '-'
  828.         elif torrent['bandwidthPriority'] == 0:
  829.             return ' '
  830.         elif torrent['bandwidthPriority'] == 1:
  831.             return '+'
  832.         else:
  833.             return '?'
  834.  
  835. # End of Class Transmission
  836.  
  837.  
  838.  
  839.  
  840.  
  841. # User Interface
  842. class Interface:
  843.     TRACKER_ITEM_HEIGHT = 6
  844.  
  845.     def __init__(self):
  846.         self.filter_list    = config.get('Filtering', 'filter')
  847.         self.filter_inverse = config.getboolean('Filtering', 'invert')
  848.         self.sort_orders    = parse_sort_str(config.get('Sorting', 'order'))
  849.         self.compact_list   = config.getboolean('Misc', 'compact_list')
  850.         self.blank_lines    = config.getboolean('Misc', 'blank_lines')
  851.         self.torrentname_is_progressbar = config.getboolean('Misc', 'torrentname_is_progressbar')
  852.         self.file_viewer    = config.get('Misc', 'file_viewer')
  853.         self.file_open_in_terminal = config.getboolean('Misc', 'file_open_in_terminal')
  854.  
  855.         self.torrents         = server.get_torrent_list(self.sort_orders)
  856.         self.stats            = server.get_global_stats()
  857.         self.torrent_details  = []
  858.         self.selected_torrent = -1  # changes to >-1 when focus >-1 & user hits return
  859.         self.highlight_dialog = False
  860.         self.search_focus = 0   # like self.focus but for searches in torrent list
  861.         self.focused_id   = -1  # the id (provided by Transmission) of self.torrents[self.focus]
  862.         self.focus        = -1  # -1: nothing focused; 0: top of list; <# of torrents>-1: bottom of list
  863.         self.scrollpos    = 0   # start of torrentlist
  864.         self.torrents_per_page  = 0 # will be set by manage_layout()
  865.         self.rateDownload_width = self.rateUpload_width = 2
  866.  
  867.         self.details_category_focus = 0  # overview/files/peers/tracker in details
  868.         self.focus_detaillist       = -1 # same as focus but for details
  869.         self.selected_files         = [] # marked files in details
  870.         self.file_index_map         = {} # Maps local torrent's file indices to server file indices
  871.         self.scrollpos_detaillist   = 0  # same as scrollpos but for details
  872.         self.compact_torrentlist    = False # draw only one line for each torrent in compact mode
  873.         self.exit_now               = False
  874.  
  875.         self.keybindings = {
  876.             ord('?'):               self.call_list_key_bindings,
  877.             curses.KEY_F1:          self.call_list_key_bindings,
  878.             27:                     self.go_back_or_unfocus,
  879.             curses.KEY_BREAK:       self.go_back_or_unfocus,
  880.             12:                     self.go_back_or_unfocus,
  881.             curses.KEY_BACKSPACE:   self.leave_details,
  882.             ord('q'):               self.go_back_or_quit,
  883.             ord('o'):               self.o_key,
  884.             ord('\n'):              self.enter_key,
  885.             curses.KEY_RIGHT:       self.right_key,
  886.             ord('l'):               self.l_key,
  887.             ord('s'):               self.show_sort_order_menu,
  888.             ord('f'):               self.f_key,
  889.             ord('u'):               self.global_upload,
  890.             ord('d'):               self.global_download,
  891.             ord('U'):               self.torrent_upload,
  892.             ord('D'):               self.torrent_download,
  893.             ord('L'):               self.seed_ratio,
  894.             ord('t'):               self.t_key,
  895.             ord('+'):               self.bandwidth_priority,
  896.             ord('-'):               self.bandwidth_priority,
  897.             ord('J'):               self.J_key,
  898.             ord('K'):               self.K_key,
  899.             ord('p'):               self.pause_unpause_torrent,
  900.             ord('P'):               self.pause_unpause_all_torrent,
  901.             ord('N'):               self.start_now_torrent,
  902.             ord('v'):               self.verify_torrent,
  903.             ord('y'):               self.verify_torrent,
  904.             ord('V'):               self.V_key,
  905.             ord('r'):               self.r_key,
  906.             curses.KEY_DC:          self.r_key,
  907.             ord('R'):               self.remove_torrent_local_data,
  908.             curses.KEY_SDC:         self.remove_torrent_local_data,
  909.             curses.KEY_UP:          self.movement_keys,
  910.             ord('k'):               self.movement_keys,
  911.             curses.KEY_DOWN:        self.movement_keys,
  912.             ord('j'):               self.movement_keys,
  913.             curses.KEY_PPAGE:       self.movement_keys,
  914.             curses.KEY_NPAGE:       self.movement_keys,
  915.             curses.KEY_HOME:        self.movement_keys,
  916.             curses.KEY_END:         self.movement_keys,
  917.             ord('g'):               self.movement_keys,
  918.             ord('G'):               self.movement_keys,
  919.             curses.ascii.ctrl(ord('f')): self.movement_keys,
  920.             curses.ascii.ctrl(ord('b')): self.movement_keys,
  921.             curses.ascii.ctrl(ord('n')): self.movement_keys,
  922.             curses.ascii.ctrl(ord('p')): self.movement_keys,
  923.             ord("\t"):              self.move_in_details,
  924.             curses.KEY_BTAB:        self.move_in_details,
  925.             ord('e'):               self.move_in_details,
  926.             ord('c'):               self.move_in_details,
  927.             ord('C'):               self.toggle_compact_torrentlist,
  928.             ord('h'):               self.file_pritority_or_switch_details,
  929.             curses.KEY_LEFT:        self.file_pritority_or_switch_details,
  930.             ord(' '):               self.space_key,
  931.             ord('a'):               self.a_key,
  932.             ord('A'):               self.A_key,
  933.             ord('m'):               self.move_torrent,
  934.             ord('n'):               self.reannounce_torrent,
  935.             ord('/'):               self.dialog_search_torrentlist,
  936.             curses.KEY_SEND:        lambda c: self.move_queue('bottom'),
  937.             curses.KEY_SHOME:       lambda c: self.move_queue('top'),
  938.             curses.KEY_SLEFT:       lambda c: self.move_queue('ppage'),
  939.             curses.KEY_SRIGHT:      lambda c: self.move_queue('npage')
  940.         }
  941.  
  942.         self.sort_options = [
  943.             ('name','_Name'), ('addedDate','_Age'), ('percentDone','_Progress'),
  944.             ('seeders','_Seeds'), ('leechers','Lee_ches'), ('sizeWhenDone', 'Si_ze'),
  945.             ('status','S_tatus'), ('uploadedEver','Up_loaded'),
  946.             ('rateUpload','_Upload Speed'), ('rateDownload','_Download Speed'),
  947.             ('uploadRatio','_Ratio'), ('peersConnected','P_eers'),
  948.             ('downloadDir', 'L_ocation'), ('mainTrackerDomain', 'Trac_ker') ]
  949.  
  950.         # queue was implemmented in transmission 2.4
  951.         if server.get_rpc_version() >= 14:
  952.             self.sort_options.append(('queuePosition', '_Queue Position'))
  953.  
  954.         self.sort_options.append(('reverse','Re_verse'))
  955.  
  956.  
  957.         try:
  958.             self.init_screen()
  959.             self.run()
  960.         except:
  961.             self.restore_screen()
  962.             (exc_type, exc_value, exc_traceback) = sys.exc_info()
  963.             raise exc_type, exc_value, exc_traceback
  964.         else:
  965.             self.restore_screen()
  966.  
  967.  
  968.     def init_screen(self):
  969.         os.environ['ESCDELAY'] = '0' # make escape usable
  970.         self.screen = curses.initscr()
  971.         curses.noecho() ; curses.cbreak() ; self.screen.keypad(1)
  972.         curses.halfdelay(10) # STDIN timeout
  973.  
  974.         hide_cursor()
  975.  
  976.         self.colors = ColorManager(dict(config.items('Colors')))
  977.  
  978.         # http://bugs.python.org/issue2675
  979.         try:
  980.             del os.environ['LINES']
  981.             del os.environ['COLUMNS']
  982.         except:
  983.             pass
  984.  
  985.         # http://bugs.python.org/issue2675
  986.         try:
  987.             del os.environ['LINES']
  988.             del os.environ['COLUMNS']
  989.         except:
  990.             pass
  991.  
  992.         signal.signal(signal.SIGWINCH, lambda y,frame: self.get_screen_size())
  993.         self.get_screen_size()
  994.  
  995.     def restore_screen(self):
  996.         curses.endwin()
  997.  
  998.     def enc(self, text):
  999.         return text.encode(ENCODING, 'replace')
  1000.  
  1001.     def get_screen_size(self):
  1002.         time.sleep(0.1) # prevents curses.error on rapid resizing
  1003.         while True:
  1004.             curses.endwin()
  1005.             self.screen.refresh()
  1006.             self.height, self.width = self.screen.getmaxyx()
  1007.             # Tracker list breaks if width smaller than 73
  1008.             if self.width < 73 or self.height < 16:
  1009.                 self.screen.erase()
  1010.                 self.screen.addstr(0,0, "Terminal too small", curses.A_REVERSE + curses.A_BOLD)
  1011.                 time.sleep(1)
  1012.             else:
  1013.                 break
  1014.         self.manage_layout()
  1015.  
  1016.     def manage_layout(self):
  1017.         self.recalculate_torrents_per_page()
  1018.         self.pad_height = max((len(self.torrents)+1) * self.tlist_item_height, self.height)
  1019.         self.pad = curses.newpad(self.pad_height, self.width)
  1020.         self.detaillistitems_per_page = self.height - 8
  1021.  
  1022.         if self.selected_torrent > -1:
  1023.             self.rateDownload_width = self.get_rateDownload_width([self.torrent_details])
  1024.             self.rateUpload_width   = self.get_rateUpload_width([self.torrent_details])
  1025.             self.torrent_title_width = self.width - self.rateUpload_width - 2
  1026.             # show downloading column only if torrents is downloading
  1027.             if self.torrent_details['status'] == Transmission.STATUS_DOWNLOAD:
  1028.                 self.torrent_title_width -= self.rateDownload_width + 2
  1029.  
  1030.         elif self.torrents:
  1031.             self.visible_torrents_start = self.scrollpos/self.tlist_item_height
  1032.             self.visible_torrents = self.torrents[self.visible_torrents_start : self.scrollpos/self.tlist_item_height + self.torrents_per_page + 1]
  1033.             self.rateDownload_width = self.get_rateDownload_width(self.visible_torrents)
  1034.             self.rateUpload_width   = self.get_rateUpload_width(self.visible_torrents)
  1035.             self.torrent_title_width = self.width - self.rateUpload_width - 2
  1036.             # show downloading column only if any downloading torrents are visible
  1037.             if filter(lambda x: x['status']==Transmission.STATUS_DOWNLOAD, self.visible_torrents):
  1038.                 self.torrent_title_width -= self.rateDownload_width + 2
  1039.         else:
  1040.             self.visible_torrents = []
  1041.             self.torrent_title_width = 80
  1042.  
  1043.     def get_rateDownload_width(self, torrents):
  1044.         new_width = max(map(lambda x: len(scale_bytes(x['rateDownload'])), torrents))
  1045.         new_width = max(max(map(lambda x: len(scale_time(x['eta'])), torrents)), new_width)
  1046.         new_width = max(len(scale_bytes(self.stats['downloadSpeed'])), new_width)
  1047.         new_width = max(self.rateDownload_width, new_width) # don't shrink
  1048.         return new_width
  1049.  
  1050.     def get_rateUpload_width(self, torrents):
  1051.         new_width = max(map(lambda x: len(scale_bytes(x['rateUpload'])), torrents))
  1052.         new_width = max(max(map(lambda x: len(num2str(x['uploadRatio'], '%.02f')), torrents)), new_width)
  1053.         new_width = max(len(scale_bytes(self.stats['uploadSpeed'])), new_width)
  1054.         new_width = max(self.rateUpload_width, new_width) # don't shrink
  1055.         return new_width
  1056.  
  1057.     def recalculate_torrents_per_page(self):
  1058.         self.lines_per_entry   = 3 if self.blank_lines else 2
  1059.         self.tlist_item_height = self.lines_per_entry if not self.compact_list else 1
  1060.         self.mainview_height = self.height - 2
  1061.         self.torrents_per_page = self.mainview_height / self.tlist_item_height
  1062.  
  1063.     def run(self):
  1064.         self.draw_title_bar()
  1065.         self.draw_stats()
  1066.         self.draw_torrent_list()
  1067.  
  1068.         while True:
  1069.             server.update(1)
  1070.  
  1071.             # I'm catching all exceptions here because resizing the terminal
  1072.             # can make a huge mess, e.g. we might be drawing to areas that
  1073.             # don't exist anymore. The proper way would probably be to write a
  1074.             # wrapper around self.pad.addstr() and anything else that uses
  1075.             # coordinates, but it's not worth the effort as this part is
  1076.             # called continuously.
  1077.             try:
  1078.                 if self.selected_torrent == -1:
  1079.                     self.draw_torrent_list()
  1080.                 else:
  1081.                     self.draw_details()
  1082.             except: pass
  1083.  
  1084.             self.stats = server.get_global_stats()
  1085.             self.draw_title_bar()  # show shortcuts and stuff
  1086.             self.draw_stats()      # show global states
  1087.             self.screen.move(0,0)  # in case cursor can't be invisible
  1088.             self.handle_user_input()
  1089.             if self.exit_now:
  1090.                 sort_str = ','.join(map(lambda x: ('','reverse:')[x['reverse']] + x['name'], self.sort_orders))
  1091.                 config.set('Sorting', 'order',   sort_str)
  1092.                 config.set('Filtering', 'filter', self.filter_list)
  1093.                 config.set('Filtering', 'invert', str(self.filter_inverse))
  1094.                 config.set('Misc', 'compact_list', str(self.compact_list))
  1095.                 config.set('Misc', 'blank_lines', str(self.blank_lines))
  1096.                 config.set('Misc', 'torrentname_is_progressbar', str(self.torrentname_is_progressbar))
  1097.                 save_config(cmd_args.configfile)
  1098.                 return
  1099.  
  1100.     def go_back_or_unfocus(self, c):
  1101.         if self.focus_detaillist > -1:   # unfocus and deselect file
  1102.             self.focus_detaillist     = -1
  1103.             self.scrollpos_detaillist = 0
  1104.             self.selected_files       = []
  1105.         elif self.selected_torrent > -1: # return from details
  1106.             self.details_category_focus = 0
  1107.             self.selected_torrent = -1
  1108.             self.selected_files   = []
  1109.         else:
  1110.             if self.focus > -1:
  1111.                 self.scrollpos = 0    # unfocus main list
  1112.                 self.focus     = -1
  1113.             elif self.filter_list:
  1114.                 self.filter_list = '' # reset filter
  1115.  
  1116.     def leave_details(self, c):
  1117.         if self.selected_torrent > -1:
  1118.             server.set_torrent_details_id(-1)
  1119.             self.selected_torrent       = -1
  1120.             self.details_category_focus = 0
  1121.             self.scrollpos_detaillist   = 0
  1122.             self.selected_files         = []
  1123.  
  1124.     def go_back_or_quit(self, c):
  1125.         if self.selected_torrent == -1:
  1126.             self.exit_now = True
  1127.         else: # return to list view
  1128.             server.set_torrent_details_id(-1)
  1129.             self.selected_torrent       = -1
  1130.             self.details_category_focus = 0
  1131.             self.focus_detaillist       = -1
  1132.             self.scrollpos_detaillist   = 0
  1133.             self.selected_files         = []
  1134.  
  1135.     def space_key(self, c):
  1136.         # File list
  1137.         if self.selected_torrent > -1 and self.details_category_focus == 1:
  1138.             self.select_unselect_file(c)
  1139.         # Torrent list
  1140.         elif self.selected_torrent == -1:
  1141.             self.enter_key(c)
  1142.  
  1143.     def A_key(self, c):
  1144.         # File list
  1145.         if self.selected_torrent > -1 and self.details_category_focus == 1:
  1146.             self.select_unselect_file(c)
  1147.         # Do nothing in other detail tabs
  1148.         elif self.selected_torrent > -1:
  1149.             pass
  1150.         else:
  1151.             self.add_torrent_by_hash()
  1152.  
  1153.     def a_key(self, c):
  1154.         # File list
  1155.         if self.selected_torrent > -1 and self.details_category_focus == 1:
  1156.             self.select_unselect_file(c)
  1157.         # Trackers
  1158.         elif self.selected_torrent > -1 and self.details_category_focus == 3:
  1159.             self.add_tracker()
  1160.  
  1161.         # Do nothing in other detail tabs
  1162.         elif self.selected_torrent > -1:
  1163.             pass
  1164.         else:
  1165.             self.add_torrent()
  1166.  
  1167.     def V_key(self, c):
  1168.         # File list
  1169.         if self.selected_torrent > -1 and self.details_category_focus == 1:
  1170.             self.select_unselect_file(c)
  1171.  
  1172.     def o_key(self, c):
  1173.         if self.selected_torrent == -1:
  1174.             self.draw_options_dialog()
  1175.         elif self.selected_torrent > -1:
  1176.             self.details_category_focus = 0
  1177.  
  1178.     def l_key(self, c):
  1179.         if self.focus > -1 and self.selected_torrent == -1:
  1180.             self.enter_key(c)
  1181.         elif self.selected_torrent > -1:
  1182.             self.file_pritority_or_switch_details(c)
  1183.  
  1184.     def t_key(self, c):
  1185.         if self.selected_torrent == -1:
  1186.             server.toggle_turtle_mode()
  1187.         elif self.selected_torrent > -1:
  1188.             self.details_category_focus = 3
  1189.  
  1190.     def f_key(self, c):
  1191.         if self.selected_torrent == -1:
  1192.             self.show_state_filter_menu(c)
  1193.         elif self.selected_torrent > -1:
  1194.             self.details_category_focus = 1
  1195.  
  1196.     def r_key(self, c):
  1197.         # Torrent list
  1198.         if self.selected_torrent == -1:
  1199.             self.remove_torrent(c)
  1200.         # Trackers
  1201.         elif self.selected_torrent > -1 and self.details_category_focus == 3:
  1202.             self.remove_tracker()
  1203.  
  1204.     def J_key(self, c):
  1205.         if self.selected_torrent > -1 and self.details_category_focus == 1:
  1206.             self.move_to_next_directory_in_filelist()
  1207.         else:
  1208.             self.move_queue('down')
  1209.  
  1210.     def K_key(self, c):
  1211.         if self.selected_torrent > -1 and self.details_category_focus == 1:
  1212.             self.move_to_previous_directory_in_filelist()
  1213.         else:
  1214.             self.move_queue('up')
  1215.  
  1216.     def right_key(self, c):
  1217.         if self.focus > -1 and self.selected_torrent == -1:
  1218.             self.enter_key(c)
  1219.         else:
  1220.             self.file_pritority_or_switch_details(c)
  1221.  
  1222.     def add_torrent(self):
  1223.         location = self.dialog_input_text("Add torrent from file or URL", homedir2tilde(os.getcwd()+os.sep), tab_complete='files')
  1224.         if location:
  1225.             error = server.add_torrent(tilde2homedir(location))
  1226.             if error:
  1227.                 msg = wrap("Couldn't add torrent \"%s\":" % location)
  1228.                 msg.extend(wrap(error, self.width-4))
  1229.                 self.dialog_ok("\n".join(msg))
  1230.  
  1231.     def add_torrent_by_hash(self):
  1232.         hash = self.dialog_input_text("Add torrent by hash")
  1233.         if hash:
  1234.             error = server.add_torrent('magnet:?xt=urn:btih:{}'.format(hash))
  1235.             if error:
  1236.                 msg = wrap("Couldn't add torrent \"%s\":" % hash)
  1237.                 msg.extend(wrap(error, self.width-4))
  1238.                 self.dialog_ok("\n".join(msg))
  1239.  
  1240.     def enter_key(self, c):
  1241.         # Torrent list
  1242.         if self.focus > -1 and self.selected_torrent == -1:
  1243.             self.screen.clear()
  1244.             self.selected_torrent = self.focus
  1245.             server.set_torrent_details_id(self.torrents[self.focus]['id'])
  1246.             server.wait_for_details_update()
  1247.         # File list
  1248.         elif self.selected_torrent > -1 and self.details_category_focus == 1:
  1249.             self.open_torrent_file(c)
  1250.  
  1251.  
  1252.     def show_sort_order_menu(self, c):
  1253.         if self.selected_torrent == -1:
  1254.            choice = self.dialog_menu('Sort order', self.sort_options,
  1255.                                      map(lambda x: x[0]==self.sort_orders[-1]['name'], self.sort_options).index(True)+1)
  1256.            if choice != -128:
  1257.                if choice == 'reverse':
  1258.                    self.sort_orders[-1]['reverse'] = not self.sort_orders[-1]['reverse']
  1259.                else:
  1260.                    self.sort_orders.append({'name':choice, 'reverse':False})
  1261.                    while len(self.sort_orders) > 2:
  1262.                        self.sort_orders.pop(0)
  1263.  
  1264.     def show_state_filter_menu(self, c):
  1265.         if self.selected_torrent == -1:
  1266.             options = [('uploading','_Uploading'), ('downloading','_Downloading'),
  1267.                        ('active','Ac_tive'), ('paused','_Paused'), ('seeding','_Seeding'),
  1268.                        ('incomplete','In_complete'), ('verifying','Verif_ying'),
  1269.                        ('private','P_rivate'), ('isolated', '_Isolated'),
  1270.                        ('invert','In_vert'), ('','_All')]
  1271.             choice = self.dialog_menu(('Show only','Filter all')[self.filter_inverse], options,
  1272.                                       map(lambda x: x[0]==self.filter_list, options).index(True)+1)
  1273.             if choice != -128:
  1274.                 if choice == 'invert':
  1275.                     self.filter_inverse = not self.filter_inverse
  1276.                 else:
  1277.                     if choice == '':
  1278.                         self.filter_inverse = False
  1279.                     self.filter_list = choice
  1280.  
  1281.     def global_upload(self, c):
  1282.        current_limit = (-1,self.stats['speed-limit-up'])[self.stats['speed-limit-up-enabled']]
  1283.        limit = self.dialog_input_number("Global upload limit in kilobytes per second", current_limit)
  1284.        if limit == -128:
  1285.            return
  1286.        server.set_rate_limit('up', limit)
  1287.  
  1288.     def global_download(self, c):
  1289.        current_limit = (-1,self.stats['speed-limit-down'])[self.stats['speed-limit-down-enabled']]
  1290.        limit = self.dialog_input_number("Global download limit in kilobytes per second", current_limit)
  1291.        if limit == -128:
  1292.            return
  1293.        server.set_rate_limit('down', limit)
  1294.  
  1295.     def torrent_upload(self, c):
  1296.         if self.focus > -1:
  1297.             current_limit = (-1,self.torrents[self.focus]['uploadLimit'])[self.torrents[self.focus]['uploadLimited']]
  1298.             limit = self.dialog_input_number("Upload limit in kilobytes per second for\n%s" % \
  1299.                                                  self.torrents[self.focus]['name'], current_limit)
  1300.             if limit == -128:
  1301.                 return
  1302.             server.set_rate_limit('up', limit, self.torrents[self.focus]['id'])
  1303.  
  1304.     def torrent_download(self, c):
  1305.         if self.focus > -1:
  1306.             current_limit = (-1,self.torrents[self.focus]['downloadLimit'])[self.torrents[self.focus]['downloadLimited']]
  1307.             limit = self.dialog_input_number("Download limit in Kilobytes per second for\n%s" % \
  1308.                                                  self.torrents[self.focus]['name'], current_limit)
  1309.             if limit == -128:
  1310.                 return
  1311.             server.set_rate_limit('down', limit, self.torrents[self.focus]['id'])
  1312.  
  1313.     def seed_ratio(self, c):
  1314.         if self.focus > -1:
  1315.             if self.torrents[self.focus]['seedRatioMode'] == 0:   # Use global settings
  1316.                 current_limit = ''
  1317.             elif self.torrents[self.focus]['seedRatioMode'] == 1: # Stop seeding at seedRatioLimit
  1318.                 current_limit = self.torrents[self.focus]['seedRatioLimit']
  1319.             elif self.torrents[self.focus]['seedRatioMode'] == 2: # Seed regardless of ratio
  1320.                 current_limit = -1
  1321.             limit = self.dialog_input_number("Seed ratio limit for\n%s" % self.torrents[self.focus]['name'],
  1322.                                              current_limit, floating_point=True, allow_empty=True)
  1323.             if limit == -1:
  1324.                 limit = 0
  1325.             if limit == -2: # -2 means 'empty' in dialog_input_number return codes
  1326.                 limit = -1
  1327.             server.set_seed_ratio(float(limit), self.torrents[self.focus]['id'])
  1328.  
  1329.     def bandwidth_priority(self, c):
  1330.         if c == ord('-') and self.focus > -1:
  1331.             server.decrease_bandwidth_priority(self.torrents[self.focus]['id'])
  1332.         elif c == ord('+') and self.focus > -1:
  1333.             server.increase_bandwidth_priority(self.torrents[self.focus]['id'])
  1334.  
  1335.     def move_queue(self, direction):
  1336.         # queue was implemmented in Transmission v2.4
  1337.         if server.get_rpc_version() >= 14 and self.focus > -1:
  1338.             if direction in ('ppage', 'npage'):
  1339.                 new_position = self.torrents[self.focus]['queuePosition']
  1340.                 if direction == 'ppage':
  1341.                     new_position -= 10
  1342.                 else:
  1343.                     new_position += 10
  1344.             else:
  1345.                 new_position = direction
  1346.             server.move_queue(self.torrents[self.focus]['id'], new_position)
  1347.  
  1348.     def pause_unpause_torrent(self, c):
  1349.         if self.focus > -1:
  1350.             if self.selected_torrent > -1:
  1351.                 t = self.torrent_details
  1352.             else:
  1353.                 t = self.torrents[self.focus]
  1354.             if t['status'] == Transmission.STATUS_STOPPED:
  1355.                 server.start_torrents([t['id']])
  1356.             else:
  1357.                 server.stop_torrents([t['id']])
  1358.  
  1359.     def start_now_torrent(self, c):
  1360.         if self.focus > -1:
  1361.             if self.selected_torrent > -1:
  1362.                 t = self.torrent_details
  1363.             else:
  1364.                 t = self.torrents[self.focus]
  1365.             server.start_now_torrent(t['id'])
  1366.  
  1367.     def pause_unpause_all_torrent(self, c):
  1368.         focused_torrent = self.torrents[ max(0,self.focus) ]
  1369.         if focused_torrent['status'] == Transmission.STATUS_STOPPED:
  1370.             server.start_torrents([t['id'] for t in self.torrents])
  1371.         else:
  1372.             server.stop_torrents([t['id'] for t in self.torrents])
  1373.  
  1374.     def verify_torrent(self, c):
  1375.         if self.focus > -1:
  1376.             if self.torrents[self.focus]['status'] != Transmission.STATUS_CHECK \
  1377.            and self.torrents[self.focus]['status'] != Transmission.STATUS_CHECK_WAIT:
  1378.                 server.verify_torrent(self.torrents[self.focus]['id'])
  1379.  
  1380.     def reannounce_torrent(self, c):
  1381.         if self.focus > -1:
  1382.             server.reannounce_torrent(self.torrents[self.focus]['id'])
  1383.  
  1384.     def remove_torrent(self, c):
  1385.         if self.focus > -1:
  1386.             name = self.torrents[self.focus]['name'][0:self.width - 15]
  1387.             if self.dialog_yesno("Remove %s?" % name) == True:
  1388.                 if self.selected_torrent > -1:  # leave details
  1389.                     server.set_torrent_details_id(-1)
  1390.                     self.selected_torrent = -1
  1391.                     self.details_category_focus = 0
  1392.                 server.remove_torrent(self.torrents[self.focus]['id'])
  1393.                 self.focus_next_after_delete()
  1394.  
  1395.     def remove_torrent_local_data(self, c):
  1396.         if self.focus > -1:
  1397.             name = self.torrents[self.focus]['name'][0:self.width - 15]
  1398.             if self.dialog_yesno("Remove and delete %s?" % name, important=True) == True:
  1399.                 if self.selected_torrent > -1:  # leave details
  1400.                     server.set_torrent_details_id(-1)
  1401.                     self.selected_torrent = -1
  1402.                     self.details_category_focus = 0
  1403.                 server.remove_torrent_local_data(self.torrents[self.focus]['id'])
  1404.                 self.focus_next_after_delete()
  1405.  
  1406.     def focus_next_after_delete(self):
  1407.         """ Focus next torrent after user deletes torrent """
  1408.         new_focus = min(self.focus + 1, len(self.torrents) - 2)
  1409.         self.focused_id = self.torrents[new_focus]['id']
  1410.  
  1411.     def add_tracker(self):
  1412.         if server.get_rpc_version() < 10:
  1413.             self.dialog_ok("You need Transmission v2.10 or higher to add trackers.")
  1414.             return
  1415.  
  1416.         tracker = self.dialog_input_text('Add tracker URL:')
  1417.         if tracker:
  1418.             t = self.torrent_details
  1419.             response = server.add_torrent_tracker(t['id'], tracker)
  1420.  
  1421.             if response:
  1422.                 msg = wrap("Couldn't add tracker: %s" % response)
  1423.                 self.dialog_ok("\n".join(msg))
  1424.  
  1425.     def remove_tracker(self):
  1426.         if server.get_rpc_version() < 10:
  1427.             self.dialog_ok("You need Transmission v2.10 or higher to remove trackers.")
  1428.             return
  1429.  
  1430.         t = self.torrent_details
  1431.         if (self.scrollpos_detaillist >= 0 and \
  1432.             self.scrollpos_detaillist < len(t['trackerStats']) and \
  1433.             self.dialog_yesno("Do you want to remove this tracker?") is True):
  1434.  
  1435.             tracker = t['trackerStats'][self.scrollpos_detaillist]
  1436.             response = server.remove_torrent_tracker(t['id'], tracker['id'])
  1437.  
  1438.             if response:
  1439.                 msg = wrap("Couldn't remove tracker: %s" % response)
  1440.                 self.dialog_ok("\n".join(msg))
  1441.  
  1442.     def movement_keys(self, c):
  1443.         if self.selected_torrent == -1 and len(self.torrents) > 0:
  1444.             if   c == curses.KEY_UP or c == ord('k') or c == curses.ascii.ctrl(ord('p')):
  1445.                 self.focus, self.scrollpos = self.move_up(self.focus, self.scrollpos, self.tlist_item_height)
  1446.             elif c == curses.KEY_DOWN or c == ord('j') or c == curses.ascii.ctrl(ord('n')):
  1447.                 self.focus, self.scrollpos = self.move_down(self.focus, self.scrollpos, self.tlist_item_height,
  1448.                                                             self.torrents_per_page, len(self.torrents))
  1449.             elif c == curses.KEY_PPAGE or c == curses.ascii.ctrl(ord('b')):
  1450.                 self.focus, self.scrollpos = self.move_page_up(self.focus, self.scrollpos, self.tlist_item_height,
  1451.                                                                self.torrents_per_page)
  1452.             elif c == curses.KEY_NPAGE or c == curses.ascii.ctrl(ord('f')):
  1453.                 self.focus, self.scrollpos = self.move_page_down(self.focus, self.scrollpos, self.tlist_item_height,
  1454.                                                                  self.torrents_per_page, len(self.torrents))
  1455.             elif c == curses.KEY_HOME or c == ord('g'):
  1456.                 self.focus, self.scrollpos = self.move_to_top()
  1457.             elif c == curses.KEY_END or c == ord('G'):
  1458.                 self.focus, self.scrollpos = self.move_to_end(self.tlist_item_height, self.torrents_per_page, len(self.torrents))
  1459.             self.focused_id = self.torrents[self.focus]['id']
  1460.         elif self.selected_torrent > -1:
  1461.             # file list
  1462.             if self.details_category_focus == 1:
  1463.                 # focus/movement
  1464.                 if c == curses.KEY_UP or c == ord('k') or c == curses.ascii.ctrl(ord('p')):
  1465.                     self.focus_detaillist, self.scrollpos_detaillist = \
  1466.                         self.move_up(self.focus_detaillist, self.scrollpos_detaillist, 1)
  1467.                 elif c == curses.KEY_DOWN or c == ord('j') or c == curses.ascii.ctrl(ord('n')):
  1468.                     self.focus_detaillist, self.scrollpos_detaillist = \
  1469.                         self.move_down(self.focus_detaillist, self.scrollpos_detaillist, 1,
  1470.                                        self.detaillistitems_per_page, len(self.torrent_details['files']))
  1471.                 elif c == curses.KEY_PPAGE or c == curses.ascii.ctrl(ord('b')):
  1472.                     self.focus_detaillist, self.scrollpos_detaillist = \
  1473.                         self.move_page_up(self.focus_detaillist, self.scrollpos_detaillist, 1,
  1474.                                           self.detaillistitems_per_page)
  1475.                 elif c == curses.KEY_NPAGE or c == curses.ascii.ctrl(ord('f')):
  1476.                     self.focus_detaillist, self.scrollpos_detaillist = \
  1477.                         self.move_page_down(self.focus_detaillist, self.scrollpos_detaillist, 1,
  1478.                                             self.detaillistitems_per_page, len(self.torrent_details['files']))
  1479.                 elif c == curses.KEY_HOME or c == ord('g'):
  1480.                     self.focus_detaillist, self.scrollpos_detaillist = self.move_to_top()
  1481.                 elif c == curses.KEY_END or c == ord('G'):
  1482.                     self.focus_detaillist, self.scrollpos_detaillist = \
  1483.                         self.move_to_end(1, self.detaillistitems_per_page, len(self.torrent_details['files']))
  1484.                 # visual mode
  1485.                 global vmode_id
  1486.                 if vmode_id > -1:
  1487.                     if vmode_id < self.focus_detaillist:
  1488.                         self.selected_files = range(vmode_id, self.focus_detaillist + 1)
  1489.                     else:
  1490.                         self.selected_files = range(self.focus_detaillist, vmode_id + 1)
  1491.             list_len = 0
  1492.  
  1493.             # peer list movement
  1494.             if self.details_category_focus == 2:
  1495.                 list_len = len(self.torrent_details['peers'])
  1496.  
  1497.             # tracker list movement
  1498.             elif self.details_category_focus == 3:
  1499.                 list_len = len(self.torrent_details['trackerStats'])
  1500.  
  1501.             # pieces list movement
  1502.             elif self.details_category_focus == 4:
  1503.                 piece_count = self.torrent_details['pieceCount']
  1504.                 margin = len(str(piece_count)) + 2
  1505.                 map_width = int(str(self.width-margin-1)[0:-1] + '0')
  1506.                 list_len = int(piece_count / map_width) + 1
  1507.  
  1508.             if list_len:
  1509.                 if c == curses.KEY_UP or c == ord('k') or c == curses.ascii.ctrl(ord('p')):
  1510.                     if self.scrollpos_detaillist > 0:
  1511.                         self.scrollpos_detaillist -= 1
  1512.                 elif c == curses.KEY_DOWN or c == ord('j') or c == curses.ascii.ctrl(ord('n')):
  1513.                     if self.scrollpos_detaillist < list_len - 1:
  1514.                         self.scrollpos_detaillist += 1
  1515.                 elif c == curses.KEY_PPAGE or c == curses.ascii.ctrl(ord('b')):
  1516.                     self.scrollpos_detaillist = \
  1517.                         max(self.scrollpos_detaillist - self.detaillistitems_per_page - 1, 0)
  1518.                 elif c == curses.KEY_NPAGE or c == curses.ascii.ctrl(ord('f')):
  1519.                     if self.scrollpos_detaillist + self.detaillistitems_per_page >= list_len:
  1520.                         self.scrollpos_detaillist = list_len - 1
  1521.                     else:
  1522.                         self.scrollpos_detaillist += self.detaillistitems_per_page
  1523.                 elif c == curses.KEY_HOME or c == ord('g'):
  1524.                     self.scrollpos_detaillist = 0
  1525.                 elif c == curses.KEY_END or c == ord('G'):
  1526.                     self.scrollpos_detaillist = list_len - 1
  1527.  
  1528.             # Disallow scrolling past the last item that would cause blank
  1529.             # space to be displayed in pieces and peer lists.
  1530.             if self.details_category_focus in (2, 4):
  1531.                 self.scrollpos_detaillist = min(self.scrollpos_detaillist,
  1532.                     max(0, list_len - self.detaillistitems_per_page))
  1533.  
  1534.     def file_pritority_or_switch_details(self, c):
  1535.         if self.selected_torrent > -1:
  1536.             # file priority OR walk through details
  1537.             if c == curses.KEY_RIGHT or c == ord('l'):
  1538.                 if self.details_category_focus == 1 and \
  1539.                         (self.selected_files or self.focus_detaillist > -1):
  1540.                     if self.selected_files:
  1541.                         files = set([self.file_index_map[index] for index in self.selected_files])
  1542.                         server.increase_file_priority(files)
  1543.                     elif self.focus_detaillist > -1:
  1544.                         server.increase_file_priority([self.file_index_map[self.focus_detaillist]])
  1545.                 else:
  1546.                     self.scrollpos_detaillist = 0
  1547.                     self.next_details()
  1548.             elif c == curses.KEY_LEFT or c == ord('h'):
  1549.                 if self.details_category_focus == 1 and \
  1550.                         (self.selected_files or self.focus_detaillist > -1):
  1551.                     if self.selected_files:
  1552.                         files = set([self.file_index_map[index] for index in self.selected_files])
  1553.                         server.decrease_file_priority(files)
  1554.                     elif self.focus_detaillist > -1:
  1555.                         server.decrease_file_priority([self.file_index_map[self.focus_detaillist]])
  1556.                 else:
  1557.                     self.scrollpos_detaillist = 0
  1558.                     self.prev_details()
  1559.  
  1560.     def select_unselect_file(self, c):
  1561.         if self.selected_torrent > -1 and self.details_category_focus == 1 and self.focus_detaillist >= 0:
  1562.             # file selection with space
  1563.             if c == ord(' '):
  1564.                 try:
  1565.                     self.selected_files.pop(self.selected_files.index(self.focus_detaillist))
  1566.                 except ValueError:
  1567.                     self.selected_files.append(self.focus_detaillist)
  1568.                 curses.ungetch(curses.KEY_DOWN) # move down
  1569.             # (un)select directory
  1570.             elif c == ord('A'):
  1571.                 file_id = self.file_index_map[self.focus_detaillist]
  1572.                 focused_dir = os.path.dirname(self.torrent_details['files'][file_id]['name'])
  1573.                 if self.selected_files.count(self.focus_detaillist):
  1574.                     for focus in range(0, len(self.torrent_details['files'])):
  1575.                         file_id = self.file_index_map[focus]
  1576.                         if self.torrent_details['files'][file_id]['name'].startswith(focused_dir):
  1577.                             try:
  1578.                                 while focus in self.selected_files:
  1579.                                     self.selected_files.remove(focus)
  1580.                             except ValueError:
  1581.                                 pass
  1582.                 else:
  1583.                     for focus in range(0, len(self.torrent_details['files'])):
  1584.                         file_id = self.file_index_map[focus]
  1585.                         if self.torrent_details['files'][file_id]['name'].startswith(focused_dir):
  1586.                             self.selected_files.append(focus)
  1587.                 self.move_to_next_directory_in_filelist()
  1588.             # (un)select all files
  1589.             elif c == ord('a'):
  1590.                 if self.selected_files:
  1591.                     self.selected_files = []
  1592.                 else:
  1593.                     self.selected_files = range(0, len(self.torrent_details['files']))
  1594.             elif c == ord('V'):
  1595.                 global vmode_id
  1596.                 if self.selected_files:
  1597.                     self.selected_files = []
  1598.                 if vmode_id != -1:
  1599.                     vmode_id = -1
  1600.                 else:
  1601.                     try:
  1602.                         self.selected_files.pop(self.selected_files.index(self.focus_detaillist))
  1603.                     except ValueError:
  1604.                         self.selected_files.append(self.focus_detaillist)
  1605.                     vmode_id = self.focus_detaillist
  1606.  
  1607.     def move_to_next_directory_in_filelist(self):
  1608.         if self.selected_torrent > -1 and self.details_category_focus == 1:
  1609.             self.focus_detaillist = max(self.focus_detaillist, 0)
  1610.             file_id = self.file_index_map[self.focus_detaillist]
  1611.             focused_dir = os.path.dirname(self.torrent_details['files'][file_id]['name'])
  1612.             while self.torrent_details['files'][file_id]['name'].startswith(focused_dir) \
  1613.                     and self.focus_detaillist < len(self.torrent_details['files'])-1:
  1614.                 self.movement_keys(curses.KEY_DOWN)
  1615.                 file_id = self.file_index_map[self.focus_detaillist]
  1616.  
  1617.     def move_to_previous_directory_in_filelist(self):
  1618.         if self.selected_torrent > -1 and self.details_category_focus == 1:
  1619.             self.focus_detaillist = max(self.focus_detaillist, 0)
  1620.             file_id = self.file_index_map[self.focus_detaillist]
  1621.             focused_dir = os.path.dirname(self.torrent_details['files'][file_id]['name'])
  1622.             while self.torrent_details['files'][file_id]['name'].startswith(focused_dir) \
  1623.                     and self.focus_detaillist > 0:
  1624.                 self.movement_keys(curses.KEY_UP)
  1625.                 file_id = self.file_index_map[self.focus_detaillist]
  1626.  
  1627.     def open_torrent_file(self, c):
  1628.         if self.focus_detaillist >= 0:
  1629.             details = server.get_torrent_details()
  1630.             stats = server.get_global_stats()
  1631.  
  1632.             file_server_index = self.file_index_map[self.focus_detaillist]
  1633.             file_name = details['files'][file_server_index]['name']
  1634.  
  1635.             download_dir = details['downloadDir']
  1636.             incomplete_dir = stats['incomplete-dir'] + '/'
  1637.        
  1638.             file_path = None
  1639.             possible_file_locations = [
  1640.                     download_dir + file_name,
  1641.                     download_dir + file_name + '.part',
  1642.                     incomplete_dir + file_name,
  1643.                     incomplete_dir + file_name + '.part'
  1644.             ]
  1645.  
  1646.             for f in possible_file_locations:
  1647.                 if (os.path.isfile(f)):
  1648.                     file_path = f
  1649.                     break
  1650.  
  1651.             if file_path is None:
  1652.                 self.get_screen_size()
  1653.                 self.dialog_ok("Could not find file:\n%s" % (file_name))
  1654.                 return
  1655.  
  1656.             viewer_cmd=[]
  1657.             for argstr in self.file_viewer.split(" "):
  1658.                 viewer_cmd.append(argstr.replace('%s', file_path))
  1659.             try:
  1660.                 if self.file_open_in_terminal:
  1661.                     self.restore_screen()
  1662.                     call(viewer_cmd)
  1663.                     self.get_screen_size()
  1664.                 else:
  1665.                     devnull = open(os.devnull, 'wb')
  1666.                     Popen(viewer_cmd, stdout=devnull, stderr=devnull)
  1667.                     devnull.close()
  1668.             except OSError, err:
  1669.                 self.get_screen_size()
  1670.                 self.dialog_ok("%s:\n%s" % (" ".join(viewer_cmd), err))
  1671.  
  1672.     def move_in_details(self, c):
  1673.         if self.selected_torrent > -1:
  1674.             if c == ord("\t"):
  1675.                 self.next_details()
  1676.             elif c == curses.KEY_BTAB:
  1677.                 self.prev_details()
  1678.             elif c == ord('e'):
  1679.                 self.details_category_focus = 2
  1680.             elif c == ord('c'):
  1681.                 self.details_category_focus = 4
  1682.  
  1683.     def call_list_key_bindings(self, c):
  1684.         self.list_key_bindings()
  1685.  
  1686.     def toggle_compact_torrentlist(self, c):
  1687.         self.compact_list = not self.compact_list
  1688.         self.recalculate_torrents_per_page()
  1689.         self.follow_list_focus()
  1690.  
  1691.     def move_torrent(self, c):
  1692.         if self.focus > -1:
  1693.             location = homedir2tilde(self.torrents[self.focus]['downloadDir'])
  1694.             msg = 'Move "%s" from\n%s to' % (self.torrents[self.focus]['name'], location)
  1695.             path = self.dialog_input_text(msg, location, tab_complete='dirs')
  1696.             if path:
  1697.                 server.move_torrent(self.torrents[self.focus]['id'], tilde2homedir(path))
  1698.  
  1699.     def handle_user_input(self):
  1700.         c = self.screen.getch()
  1701.         if c == -1:
  1702.             return 0
  1703.         f = self.keybindings.get(c, None)
  1704.         if f:
  1705.             f(c)
  1706.         try:
  1707.             if self.selected_torrent == -1:
  1708.                 self.draw_torrent_list()
  1709.             else:
  1710.                 self.draw_details()
  1711.         except: pass
  1712.  
  1713.     def filter_torrent_list(self):
  1714.         unfiltered = self.torrents
  1715.         if self.filter_list == 'downloading':
  1716.             self.torrents = [t for t in self.torrents if t['rateDownload'] > 0]
  1717.         elif self.filter_list == 'uploading':
  1718.             self.torrents = [t for t in self.torrents if t['rateUpload'] > 0]
  1719.         elif self.filter_list == 'paused':
  1720.             self.torrents = [t for t in self.torrents if t['status'] == Transmission.STATUS_STOPPED]
  1721.         elif self.filter_list == 'seeding':
  1722.             self.torrents = [t for t in self.torrents if t['status'] == Transmission.STATUS_SEED \
  1723.                                  or t['status'] == Transmission.STATUS_SEED_WAIT]
  1724.         elif self.filter_list == 'incomplete':
  1725.             self.torrents = [t for t in self.torrents if t['percentDone'] < 100]
  1726.         elif self.filter_list == 'private':
  1727.             self.torrents = [t for t in self.torrents if t['isPrivate']]
  1728.         elif self.filter_list == 'active':
  1729.             self.torrents = [t for t in self.torrents if t['peersGettingFromUs'] > 0 \
  1730.                                  or t['peersSendingToUs'] > 0 \
  1731.                                  or t['status'] == Transmission.STATUS_CHECK]
  1732.         elif self.filter_list == 'verifying':
  1733.             self.torrents = [t for t in self.torrents if t['status'] == Transmission.STATUS_CHECK \
  1734.                                  or t['status'] == Transmission.STATUS_CHECK_WAIT]
  1735.         elif self.filter_list == 'isolated':
  1736.             self.torrents = [t for t in self.torrents if t['isIsolated']]
  1737.         # invert list?
  1738.         if self.filter_inverse:
  1739.             self.torrents = [t for t in unfiltered if t not in self.torrents]
  1740.  
  1741.     def follow_list_focus(self):
  1742.         if self.focus == -1:
  1743.             return
  1744.  
  1745.         # check if list is empty or id to look for isn't in list
  1746.         ids = [t['id'] for t in self.torrents]
  1747.         if len(self.torrents) == 0 or self.focused_id not in ids:
  1748.             self.focus, self.scrollpos = -1, 0
  1749.             return
  1750.  
  1751.         # find focused_id
  1752.         self.focus = min(self.focus, len(self.torrents)-1)
  1753.         if self.torrents[self.focus]['id'] != self.focused_id:
  1754.             for i,t in enumerate(self.torrents):
  1755.                 if t['id'] == self.focused_id:
  1756.                     self.focus = i
  1757.                     break
  1758.  
  1759.         # make sure the focus is not above the visible area
  1760.         while self.focus < (self.scrollpos/self.tlist_item_height):
  1761.             self.scrollpos -= self.tlist_item_height
  1762.         # make sure the focus is not below the visible area
  1763.         while self.focus > (self.scrollpos/self.tlist_item_height) + self.torrents_per_page-1:
  1764.             self.scrollpos += self.tlist_item_height
  1765.         # keep min and max bounds
  1766.         self.scrollpos = min(self.scrollpos, (len(self.torrents) - self.torrents_per_page) * self.tlist_item_height)
  1767.         self.scrollpos = max(0, self.scrollpos)
  1768.  
  1769.     def draw_torrent_list(self, search_keyword=''):
  1770.         self.torrents = server.get_torrent_list(self.sort_orders)
  1771.         self.filter_torrent_list()
  1772.  
  1773.         if search_keyword:
  1774.             matched_torrents = [t for t in self.torrents if search_keyword.lower() in t['name'].lower()]
  1775.             if matched_torrents:
  1776.                 self.focus = 0
  1777.                 if self.search_focus >= len(matched_torrents):
  1778.                     self.search_focus = 0
  1779.                 self.focused_id = matched_torrents[self.search_focus]['id']
  1780.                 self.highlight_dialog = False
  1781.             else:
  1782.                 self.highlight_dialog = True
  1783.                 curses.beep()
  1784.         else:
  1785.             self.search_focus = 0
  1786.  
  1787.         self.follow_list_focus()
  1788.         self.manage_layout()
  1789.  
  1790.         ypos = 0
  1791.         for i in range(len(self.visible_torrents)):
  1792.             ypos += self.draw_torrentlist_item(self.visible_torrents[i],
  1793.                                                (i == self.focus-self.visible_torrents_start),
  1794.                                                self.compact_list,
  1795.                                                ypos)
  1796.  
  1797.         self.pad.refresh(0,0, 1,0, self.mainview_height,self.width-1)
  1798.         self.screen.refresh()
  1799.  
  1800.  
  1801.     def draw_torrentlist_item(self, torrent, focused, compact, y):
  1802.         # the torrent name is also a progress bar
  1803.         self.draw_torrentlist_title(torrent, focused, self.torrent_title_width, y)
  1804.  
  1805.         rates = ''
  1806.         if torrent['status'] == Transmission.STATUS_DOWNLOAD:
  1807.             self.draw_downloadrate(torrent, y)
  1808.         if torrent['status'] == Transmission.STATUS_DOWNLOAD or torrent['status'] == Transmission.STATUS_SEED:
  1809.             self.draw_uploadrate(torrent, y)
  1810.  
  1811.         if not compact:
  1812.             # the line below the title/progress
  1813.             if torrent['percentDone'] < 100 and torrent['status'] == Transmission.STATUS_DOWNLOAD:
  1814.                 self.draw_eta(torrent, y)
  1815.  
  1816.             self.draw_ratio(torrent, y)
  1817.             self.draw_torrentlist_status(torrent, focused, y)
  1818.  
  1819.             return self.lines_per_entry # number of lines that were used for drawing the list item
  1820.         else:
  1821.             # Draw ratio in place of upload rate if upload rate = 0
  1822.             if not torrent['rateUpload']:
  1823.                 self.draw_ratio(torrent, y - 1)
  1824.  
  1825.             return 1
  1826.  
  1827.     def draw_downloadrate(self, torrent, ypos):
  1828.     curses_tags = curses.color_pair(self.colors.id('title_download'))
  1829.         self.pad.move(ypos, self.width-self.rateDownload_width-self.rateUpload_width-3)
  1830.         self.pad.addch(curses.ACS_DARROW, (0,curses.A_BOLD)[torrent['downloadLimited']])
  1831.         rate = ('',scale_bytes(torrent['rateDownload']))[torrent['rateDownload']>0]
  1832.         self.pad.addstr(rate.rjust(self.rateDownload_width),
  1833.                         curses_tags + curses.A_BOLD)
  1834.     def draw_uploadrate(self, torrent, ypos):
  1835.     curses_tags = curses.color_pair(self.colors.id('filter_status'))
  1836.         self.pad.move(ypos, self.width-self.rateUpload_width-1)
  1837.         self.pad.addch(curses.ACS_UARROW, (0,curses.A_BOLD)[torrent['uploadLimited']])
  1838.         rate = ('',scale_bytes(torrent['rateUpload']))[torrent['rateUpload']>0]
  1839.         self.pad.addstr(rate.rjust(self.rateUpload_width),
  1840.                         curses_tags + curses.A_BOLD)
  1841.     def draw_ratio(self, torrent, ypos):
  1842.     curses_tags = curses.color_pair(self.colors.id('filter_status'))
  1843.         self.pad.addch(ypos+1, self.width-self.rateUpload_width-1, curses.ACS_DIAMOND,
  1844.                        (0,curses.A_BOLD)[torrent['uploadRatio'] < 1 and torrent['uploadRatio'] >= 0])
  1845.         self.pad.addstr(ypos+1, self.width-self.rateUpload_width,
  1846.                         num2str(torrent['uploadRatio'], '%.02f').rjust(self.rateUpload_width),
  1847.                         curses_tags + curses.A_BOLD)
  1848.     def draw_eta(self, torrent, ypos):
  1849.     curses_tags = curses.color_pair(self.colors.id('title_download'))
  1850.         self.pad.addch(ypos+1, self.width-self.rateDownload_width-self.rateUpload_width-3, curses.ACS_PLMINUS)
  1851.         self.pad.addstr(ypos+1, self.width-self.rateDownload_width-self.rateUpload_width-2,
  1852.                         scale_time(torrent['eta']).rjust(self.rateDownload_width),
  1853.                         curses_tags + curses.A_BOLD)
  1854.  
  1855.  
  1856.     def draw_torrentlist_title(self, torrent, focused, width, ypos):
  1857.         if torrent['status'] == Transmission.STATUS_CHECK:
  1858.             percentDone = float(torrent['recheckProgress']) * 100
  1859.         else:
  1860.             percentDone = torrent['percentDone']
  1861.  
  1862.         bar_width = int(float(width) * (float(percentDone)/100))
  1863.  
  1864.         size = "%6s" % scale_bytes(torrent['sizeWhenDone'])
  1865.         if torrent['percentDone'] < 100:
  1866.             if torrent['seeders'] <= 0 and torrent['status'] != Transmission.STATUS_CHECK:
  1867.                 size = "%6s / " % scale_bytes(torrent['available']) + size
  1868.             size = "%6s / " % scale_bytes(torrent['haveValid'] + torrent['haveUnchecked']) + size
  1869.         size = ' ' + size
  1870.         title = ljust_columns(torrent['name'], width - len(size)) + size
  1871.  
  1872.         if torrent['isIsolated']:
  1873.             color = curses.color_pair(self.colors.id('title_error'))
  1874.         elif torrent['status'] == Transmission.STATUS_SEED or \
  1875.            torrent['status'] == Transmission.STATUS_SEED_WAIT:
  1876.             color = curses.color_pair(self.colors.id('title_seed'))
  1877.         elif torrent['status'] == Transmission.STATUS_STOPPED:
  1878.             color = curses.color_pair(self.colors.id('title_paused'))
  1879.         elif torrent['status'] == Transmission.STATUS_CHECK or \
  1880.              torrent['status'] == Transmission.STATUS_CHECK_WAIT:
  1881.             color = curses.color_pair(self.colors.id('title_verify'))
  1882.         elif torrent['rateDownload'] == 0:
  1883.             color = curses.color_pair(self.colors.id('title_idle'))
  1884.         elif torrent['percentDone'] < 100:
  1885.             color = curses.color_pair(self.colors.id('title_download'))
  1886.         else:
  1887.             color = 0
  1888.  
  1889.         tag = curses.A_REVERSE
  1890.         tag_done = tag + color
  1891.         if focused:
  1892.             tag += curses.A_BOLD
  1893.             tag_done += curses.A_BOLD
  1894.  
  1895.         if self.torrentname_is_progressbar:
  1896.             # Estimate widths, which works for anything ASCII
  1897.             bar_complete = title[:bar_width]
  1898.             bar_incomplete = title[bar_width:]
  1899.             # Adjust for East-Asian (wide) characters
  1900.             while len_columns(bar_complete) != bar_width:
  1901.                 if len_columns(bar_complete) > bar_width:
  1902.                     bar_incomplete = bar_complete[-1] + bar_incomplete
  1903.                     bar_complete = bar_complete[:-1]
  1904.                 else:
  1905.                     bar_complete += bar_incomplete[0]
  1906.                     bar_incomplete = bar_incomplete[1:]
  1907.             self.pad.addstr(ypos, 0, self.enc(bar_complete), tag_done)
  1908.             self.pad.addstr(ypos, len_columns(bar_complete), self.enc(bar_incomplete))
  1909.         else:
  1910.             self.pad.addstr(ypos, 0, self.enc(title), tag_done)
  1911.  
  1912.  
  1913.     def draw_torrentlist_status(self, torrent, focused, ypos):
  1914.         peers = ''
  1915.         parts = [server.get_status(torrent)]
  1916.  
  1917.         if torrent['isIsolated'] and torrent['peersConnected'] <= 0:
  1918.             if not torrent['trackerStats']:
  1919.                 parts[0] = "Unable to find peers without trackers and DHT disabled"
  1920.             else:
  1921.                 tracker_errors = [tracker['lastAnnounceResult'] or tracker['lastScrapeResult']
  1922.                                   for tracker in torrent['trackerStats']]
  1923.                 parts[0] = self.enc([te for te in tracker_errors if te][0])
  1924.         else:
  1925.             if torrent['status'] == Transmission.STATUS_CHECK:
  1926.                 parts[0] += " (%d%%)" % int(float(torrent['recheckProgress']) * 100)
  1927.             elif torrent['status'] == Transmission.STATUS_DOWNLOAD:
  1928.                 parts[0] += " (%d%%)" % torrent['percentDone']
  1929.             parts[0] = parts[0].ljust(20)
  1930.  
  1931.             # seeds and leeches will be appended right justified later
  1932.             peers  = "%5s seed%s " % (num2str(torrent['seeders']), ('s', ' ')[torrent['seeders']==1])
  1933.             peers += "%5s leech%s" % (num2str(torrent['leechers']), ('es', '  ')[torrent['leechers']==1])
  1934.  
  1935.             # show additional information if enough room
  1936.             if self.torrent_title_width - sum(map(lambda x: len(x), parts)) - len(peers) > 18:
  1937.                 uploaded = scale_bytes(torrent['uploadedEver'])
  1938.                 parts.append("%7s uploaded" % ('nothing',uploaded)[uploaded != '0B'])
  1939.  
  1940.             if self.torrent_title_width - sum(map(lambda x: len(x), parts)) - len(peers) > 22:
  1941.                 parts.append("%4s peer%s connected" % (torrent['peersConnected'],
  1942.                                                        ('s',' ')[torrent['peersConnected'] == 1]))
  1943.  
  1944.         if focused: tags = curses.A_REVERSE + curses.A_BOLD
  1945.         else:       tags = 0
  1946.  
  1947.         remaining_space = self.torrent_title_width - sum(map(lambda x: len(x), parts), len(peers)) - 2
  1948.         delimiter = ' ' * int(remaining_space / (len(parts)))
  1949.  
  1950.         line = server.get_bandwidth_priority(torrent) + ' ' + delimiter.join(parts)
  1951.  
  1952.         # make sure the peers element is always right justified
  1953.         line += ' ' * int(self.torrent_title_width - len(line) - len(peers)) + peers
  1954.         self.pad.addstr(ypos+1, 0, line, tags)
  1955.  
  1956.  
  1957.     def draw_details(self):
  1958.         self.torrent_details = server.get_torrent_details()
  1959.         self.manage_layout()
  1960.  
  1961.         # details could need more space than the torrent list
  1962.         self.pad_height = max(50, len(self.torrent_details['files'])+10, (len(self.torrents)+1)*3, self.height)
  1963.         self.pad = curses.newpad(self.pad_height, self.width)
  1964.  
  1965.         # torrent name + progress bar
  1966.         self.draw_torrentlist_item(self.torrent_details, False, False, 0)
  1967.  
  1968.         # divider + menu
  1969.         menu_items = ['_Overview', "_Files", 'P_eers', '_Trackers', 'Pie_ces' ]
  1970.         xpos = int((self.width - sum(map(lambda x: len(x), menu_items))-len(menu_items)) / 2)
  1971.         for item in menu_items:
  1972.             self.pad.move(3, xpos)
  1973.             tags = curses.A_BOLD
  1974.             if menu_items.index(item) == self.details_category_focus:
  1975.                 tags += curses.A_REVERSE
  1976.             title = item.split('_')
  1977.             self.pad.addstr(title[0], tags)
  1978.             self.pad.addstr(title[1][0], tags + curses.A_UNDERLINE)
  1979.             self.pad.addstr(title[1][1:], tags)
  1980.             xpos += len(item)+1
  1981.  
  1982.         # which details to display
  1983.         if self.details_category_focus == 0:
  1984.             self.draw_details_overview(5)
  1985.         elif self.details_category_focus == 1:
  1986.             self.draw_filelist(5)
  1987.         elif self.details_category_focus == 2:
  1988.             self.draw_peerlist(5)
  1989.         elif self.details_category_focus == 3:
  1990.             self.draw_trackerlist(5)
  1991.         elif self.details_category_focus == 4:
  1992.             self.draw_pieces_map(5)
  1993.  
  1994.         self.pad.refresh(0,0, 1,0, self.height-2,self.width)
  1995.         self.screen.refresh()
  1996.  
  1997.  
  1998.     def draw_details_overview(self, ypos):
  1999.         t = self.torrent_details
  2000.         info = []
  2001.         info.append(['Hash: ', "%s" % t['hashString']])
  2002.         info.append(['ID: ',   "%s" % t['id']])
  2003.  
  2004.         wanted = 0
  2005.         for i, file_info in enumerate(t['files']):
  2006.             if t['wanted'][i] == True: wanted += t['files'][i]['length']
  2007.  
  2008.         sizes = ['Size: ', "%s;  " % scale_bytes(t['totalSize'], 'long'),
  2009.                  "%s wanted;  " % (scale_bytes(wanted, 'long'),'everything') [t['totalSize'] == wanted]]
  2010.         if t['available'] < t['totalSize']:
  2011.             sizes.append("%s available;  " % scale_bytes(t['available'], 'long'))
  2012.         sizes.extend(["%s left" % scale_bytes(t['leftUntilDone'], 'long')])
  2013.         info.append(sizes)
  2014.  
  2015.         info.append(['Files: ', "%d;  " % len(t['files'])])
  2016.         complete     = map(lambda x: x['bytesCompleted'] == x['length'], t['files']).count(True)
  2017.         not_complete = filter(lambda x: x['bytesCompleted'] != x['length'], t['files'])
  2018.         partial      = map(lambda x: x['bytesCompleted'] > 0, not_complete).count(True)
  2019.         if complete == len(t['files']):
  2020.             info[-1].append("all complete")
  2021.         else:
  2022.             info[-1].append("%d complete;  " % complete)
  2023.             info[-1].append("%d commenced" % partial)
  2024.  
  2025.         info.append(['Pieces: ', "%s;  " % t['pieceCount'],
  2026.                      "%s each" % scale_bytes(t['pieceSize'], 'long')])
  2027.  
  2028.         info.append(['Download: '])
  2029.         info[-1].append("%s" % scale_bytes(t['downloadedEver'], 'long') + \
  2030.                         " (%d%%) received;  " % int(percent(t['sizeWhenDone'], t['downloadedEver'])))
  2031.         info[-1].append("%s" % scale_bytes(t['haveValid'], 'long') + \
  2032.                         " (%d%%) verified;  " % int(percent(t['sizeWhenDone'], t['haveValid'])))
  2033.         info[-1].append("%s corrupt"  % scale_bytes(t['corruptEver'], 'long'))
  2034.         if t['percentDone'] < 100:
  2035.             info[-1][-1] += ';  '
  2036.             if t['rateDownload']:
  2037.                 info[-1].append("receiving %s per second" % scale_bytes(t['rateDownload'], 'long'))
  2038.                 if t['downloadLimited']:
  2039.                     info[-1][-1] += " (throttled to %s)" % scale_bytes(t['downloadLimit']*1024, 'long')
  2040.             else:
  2041.                 info[-1].append("no reception in progress")
  2042.  
  2043.         try:
  2044.             copies_distributed = (float(t['uploadedEver']) / float(t['sizeWhenDone']))
  2045.         except ZeroDivisionError:
  2046.             copies_distributed = 0
  2047.         info.append(['Upload: ', "%s (%d%%) transmitted; " %
  2048.                      (scale_bytes(t['uploadedEver'], 'long'), t['uploadRatio']*100)])
  2049.         if t['rateUpload']:
  2050.             info[-1].append("sending %s per second" % scale_bytes(t['rateUpload'], 'long'))
  2051.             if t['uploadLimited']:
  2052.                 info[-1][-1] += " (throttled to %s)" % scale_bytes(t['uploadLimit']*1024, 'long')
  2053.         else:
  2054.             info[-1].append("no transmission in progress")
  2055.  
  2056.         info.append(['Ratio: ', '%.2f copies distributed' % copies_distributed])
  2057.         norm_upload_rate = norm.add('%s:rateUpload' % t['id'], t['rateUpload'], 50)
  2058.         if norm_upload_rate > 0:
  2059.             target_ratio = self.get_target_ratio()
  2060.             bytes_left   = (max(t['downloadedEver'],t['sizeWhenDone']) * target_ratio) - t['uploadedEver']
  2061.             time_left    = bytes_left / norm_upload_rate
  2062.             info[-1][-1] += ';  '
  2063.             if time_left < 86400:   # 1 day
  2064.                 info[-1].append('approaching %.2f at %s' % \
  2065.                                     (target_ratio, timestamp(time.time() + time_left, "%R")))
  2066.             else:
  2067.                 info[-1].append('approaching %.2f on %s' % \
  2068.                                     (target_ratio, timestamp(time.time() + time_left, "%x")))
  2069.  
  2070.         info.append(['Seed limit: '])
  2071.         if t['seedRatioMode'] == 0:
  2072.             if self.stats['seedRatioLimited']:
  2073.                 info[-1].append('default (pause torrent after distributing %s copies)' % self.stats['seedRatioLimit'])
  2074.             else:
  2075.                 info[-1].append('default (unlimited)')
  2076.         elif t['seedRatioMode'] == 1:
  2077.             info[-1].append('pause torrent after distributing %s copies' % t['seedRatioLimit'])
  2078.         elif t['seedRatioMode'] == 2:
  2079.             info[-1].append('unlimited (ignore global limits)')
  2080.  
  2081.         info.append(['Peers: ',
  2082.                      "connected to %d;  "     % t['peersConnected'],
  2083.                      "downloading from %d;  " % t['peersSendingToUs'],
  2084.                      "uploading to %d"        % t['peersGettingFromUs']])
  2085.  
  2086.         # average peer speed
  2087.         incomplete_peers = [peer for peer in self.torrent_details['peers'] if peer['progress'] < 1]
  2088.         if incomplete_peers:
  2089.             # use at least 2/3 or 10 of incomplete peers to make an estimation
  2090.             active_peers = [peer for peer in incomplete_peers if peer['download_speed']]
  2091.             min_active_peers = min(10, max(1, round(len(incomplete_peers)*0.666)))
  2092.             if 1 <= len(active_peers) >= min_active_peers:
  2093.                 swarm_speed  = sum([peer['download_speed'] for peer in active_peers]) / len(active_peers)
  2094.                 info.append(['Swarm speed: ', "%s on average;  " % scale_bytes(swarm_speed),
  2095.                              "distribution of 1 copy takes %s" % \
  2096.                                  scale_time(int(t['totalSize'] / swarm_speed), 'long')])
  2097.             else:
  2098.                 info.append(['Swarm speed: ', "<gathering info from %d peers, %d done>" % \
  2099.                                  (min_active_peers, len(active_peers))])
  2100.         else:
  2101.             info.append(['Swarm speed: ', "<no downloading peers connected>"])
  2102.  
  2103.  
  2104.         info.append(['Privacy: '])
  2105.         if t['isPrivate']:
  2106.             info[-1].append('Private to this tracker -- DHT and PEX disabled')
  2107.         else:
  2108.             info[-1].append('Public torrent')
  2109.  
  2110.         info.append(['Location: ',"%s" % homedir2tilde(t['downloadDir'])])
  2111.  
  2112.         if t['creator']:
  2113.             info.append(['Creator: ',"%s" % t['creator']])
  2114.  
  2115.         ypos = self.draw_details_list(ypos, info)
  2116.  
  2117.         self.draw_details_eventdates(ypos+1)
  2118.         return ypos+1
  2119.  
  2120.     def get_target_ratio(self):
  2121.         t = self.torrent_details
  2122.         if t['seedRatioMode'] == 1:
  2123.             return t['seedRatioLimit']              # individual limit
  2124.         elif t['seedRatioMode'] == 0 and self.stats['seedRatioLimited']:
  2125.             return self.stats['seedRatioLimit']     # global limit
  2126.         else:
  2127.             # round up to next 10/5/1
  2128.             if t['uploadRatio'] >= 100:
  2129.                 step_size = 10.0
  2130.             elif t['uploadRatio'] >= 10:
  2131.                 step_size = 5.0
  2132.             else:
  2133.                 step_size = 1.0
  2134.             return int(round((t['uploadRatio'] + step_size/2) / step_size) * step_size)
  2135.  
  2136.     def draw_details_eventdates(self, ypos):
  2137.         t = self.torrent_details
  2138.  
  2139.         self.pad.addstr(ypos,   1, '  Created: ' + timestamp(t['dateCreated']))
  2140.         self.pad.addstr(ypos+1, 1, '    Added: ' + timestamp(t['addedDate']))
  2141.         self.pad.addstr(ypos+2, 1, '  Started: ' + timestamp(t['startDate']))
  2142.         self.pad.addstr(ypos+3, 1, ' Activity: ' + timestamp(t['activityDate']))
  2143.  
  2144.         if t['percentDone'] < 100 and t['eta'] > 0:
  2145.             self.pad.addstr(ypos+4, 1, 'Finishing: ' + timestamp(time.time() + t['eta']))
  2146.         elif t['doneDate'] <= 0:
  2147.             self.pad.addstr(ypos+4, 1, 'Finishing: sometime')
  2148.         else:
  2149.             self.pad.addstr(ypos+4, 1, ' Finished: ' + timestamp(t['doneDate']))
  2150.  
  2151.         if t['comment']:
  2152.             if self.width >= 90:
  2153.                 width = self.width - 50
  2154.                 comment = wrap_multiline(t['comment'], width, initial_indent='Comment: ')
  2155.                 for i, line in enumerate(comment):
  2156.                     if(ypos+i > self.height-1):
  2157.                         break
  2158.                     self.pad.addstr(ypos+i, 50, self.enc(line))
  2159.             else:
  2160.                 width = self.width - 2
  2161.                 comment = wrap_multiline(t['comment'], width, initial_indent='Comment: ')
  2162.                 for i, line in enumerate(comment):
  2163.                     self.pad.addstr(ypos+6+i, 2, self.enc(line))
  2164.  
  2165.     def draw_filelist(self, ypos):
  2166.         column_names = '  #  Progress  Size  Priority  Filename'
  2167.         self.pad.addstr(ypos, 0, column_names.ljust(self.width))
  2168.         ypos += 1
  2169.  
  2170.         for line in self.create_filelist():
  2171.             curses_tags = 0
  2172.             # highlight focused/selected line(s)
  2173.             while line.startswith('_'):
  2174.                 if line[1] == 'S':
  2175.                     curses_tags  = curses.A_BOLD
  2176.                     line = line[2:]
  2177.                 if line[1] == 'F':
  2178.                     curses_tags += curses.A_REVERSE
  2179.                     line = line[2:]
  2180.                 try:
  2181.                     self.pad.addstr(ypos, 0, ' '*self.width, curses_tags)
  2182.                 except: pass
  2183.  
  2184.             # colored priority (only in the first 30 chars, the rest is filename)
  2185.             xpos = 0
  2186.             for part in re.split('(high|normal|low|off)', line[0:30], 1):
  2187.                 if part == 'high':
  2188.                     self.pad.addstr(ypos, xpos, self.enc(part),
  2189.                                     curses_tags + curses.color_pair(self.colors.id('file_prio_high')))
  2190.                 elif part == 'normal':
  2191.                     self.pad.addstr(ypos, xpos, self.enc(part),
  2192.                                     curses_tags + curses.color_pair(self.colors.id('file_prio_normal')))
  2193.                 elif part == 'low':
  2194.                     self.pad.addstr(ypos, xpos, self.enc(part),
  2195.                                     curses_tags + curses.color_pair(self.colors.id('file_prio_low')))
  2196.                 elif part == 'off':
  2197.                     self.pad.addstr(ypos, xpos, self.enc(part),
  2198.                                     curses_tags + curses.color_pair(self.colors.id('file_prio_off')))
  2199.                 else:
  2200.                     self.pad.addstr(ypos, xpos, self.enc(part), curses_tags)
  2201.                 xpos += len(part)
  2202.             self.pad.addstr(ypos, xpos, self.enc(line[30:]), curses_tags)
  2203.             ypos += 1
  2204.             if ypos > self.height:
  2205.                 break
  2206.  
  2207.     def create_filelist(self):
  2208.         files = sorted(self.torrent_details['files'], cmp=lambda x,y: cmp(x['name'], y['name']))
  2209.         # Build new mapping between sorted local files and transmission-daemon's unsorted files
  2210.         self.file_index_map = {}
  2211.         for index,file in enumerate(files):
  2212.             self.file_index_map[index] = self.torrent_details['files'].index(file)
  2213.  
  2214.         filelist = []
  2215.         current_folder = []
  2216.         current_depth = 0
  2217.         pos = 0
  2218.         pos_before_focus = 0
  2219.         index = 0
  2220.         for file in files:
  2221.             f = file['name'].split('/')
  2222.             f_len = len(f) - 1
  2223.             if f[:f_len] != current_folder:
  2224.                 [current_depth, pos] = self.create_filelist_transition(f, current_folder, filelist, current_depth, pos)
  2225.                 current_folder = f[:f_len]
  2226.             filelist.append(self.create_filelist_line(f[-1], index, percent(file['length'], file['bytesCompleted']),
  2227.                 file['length'], current_depth))
  2228.             index += 1
  2229.             if self.focus_detaillist == index - 1:
  2230.                 pos_before_focus = pos
  2231.             if index + pos >= self.focus_detaillist + 1 + pos + self.detaillistitems_per_page/2 \
  2232.             and index + pos >= self.detaillistitems_per_page:
  2233.                 if self.focus_detaillist + 1 + pos_before_focus < self.detaillistitems_per_page / 2:
  2234.                     return filelist
  2235.                 return filelist[self.focus_detaillist + 1 + pos_before_focus - self.detaillistitems_per_page / 2
  2236.                         : self.focus_detaillist + 1 + pos_before_focus + self.detaillistitems_per_page / 2]
  2237.         begin = len(filelist) - self.detaillistitems_per_page
  2238.         return filelist[begin > 0 and begin or 0:]
  2239.  
  2240.     def create_filelist_transition(self, f, current_folder, filelist, current_depth, pos):
  2241.         """ Create directory transition from <current_folder> to <f>,
  2242.        both of which are an array of strings, each one representing one
  2243.        subdirectory in their path (e.g. /tmp/a/c would result in
  2244.        [temp, a, c]). <filelist> is a list of strings that will later be drawn
  2245.        to screen. This function only creates directory strings, and is
  2246.        responsible for managing depth (i.e. indentation) between different
  2247.        directories.
  2248.        """
  2249.         f_len = len(f) - 1  # Amount of subdirectories in f
  2250.         current_folder_len = len(current_folder)  # Amount of subdirectories in
  2251.                                                   # current_folder
  2252.         # Number of directory parts from f and current_directory that are identical
  2253.         same = 0
  2254.         while (same < current_folder_len and
  2255.                same < f_len and
  2256.                f[same] == current_folder[same]):
  2257.             same += 1
  2258.  
  2259.         # Reduce depth for each directory f has less than current_folder
  2260.         if self.blank_lines:
  2261.             for i in range(current_folder_len - same):
  2262.                current_depth -= 1
  2263.                filelist.append('  '*current_depth + ' '*31 + '/')
  2264.                pos += 1
  2265.         else: # code duplication, but less calculation
  2266.             for i in range(current_folder_len - same):
  2267.                current_depth -= 1
  2268.  
  2269.         # Stepping out of a directory, but not into a new directory
  2270.         if f_len < current_folder_len and f_len == same:
  2271.             return [current_depth, pos]
  2272.  
  2273.         # Increase depth for each new directory that appears in f,
  2274.         # but not in current_directory
  2275.         while current_depth < f_len:
  2276.             filelist.append('%s\\ %s' % ('  '*current_depth + ' '*31 , f[current_depth]))
  2277.             current_depth += 1
  2278.             pos += 1
  2279.         return [current_depth, pos]
  2280.  
  2281.     def create_filelist_line(self, name, index, percent, length, current_depth):
  2282.         line = "%s  %6.1f%%" % (str(index+1).rjust(3), percent) + \
  2283.             '  '+scale_bytes(length).rjust(5) + \
  2284.             '  '+server.get_file_priority(self.torrent_details['id'], self.file_index_map[index]).center(8) + \
  2285.             " %s| %s" % ('  '*current_depth, name[0:self.width-31-current_depth])
  2286.         if index == self.focus_detaillist:
  2287.             line = '_F' + line
  2288.         if index in self.selected_files:
  2289.             line = '_S' + line
  2290.         return line
  2291.  
  2292.     def draw_peerlist(self, ypos):
  2293.         # Start drawing list either at the "selected" index, or at the index
  2294.         # that is required to display all remaining items without further scrolling.
  2295.         last_possible_index = max(0, len(self.torrent_details['peers']) - self.detaillistitems_per_page)
  2296.         start = min(self.scrollpos_detaillist, last_possible_index)
  2297.         end = start + self.detaillistitems_per_page
  2298.         peers = self.torrent_details['peers'][start:end]
  2299.  
  2300.         # Find width of columns
  2301.         clientname_width = 0
  2302.         address_width = 0
  2303.         port_width = 0
  2304.         for peer in peers:
  2305.             if len(peer['clientName']) > clientname_width: clientname_width = len(peer['clientName'])
  2306.             if len(peer['address']) > address_width: address_width = len(peer['address'])
  2307.             if len(str(peer['port'])) > port_width: port_width = len(str(peer['port']))
  2308.  
  2309.         # Column names
  2310.         column_names = 'Flags %3d Down %3d Up Progress     ETA ' % \
  2311.             (self.torrent_details['peersSendingToUs'], self.torrent_details['peersGettingFromUs'])
  2312.         column_names += 'Client'.ljust(clientname_width + 1) \
  2313.             + 'Address'.ljust(address_width+port_width+1)
  2314.         if features['geoip']: column_names += 'Country'
  2315.         if features['dns']: column_names += ' Host'
  2316.  
  2317.         self.pad.addstr(ypos, 0, column_names.ljust(self.width))
  2318.         ypos += 1
  2319.  
  2320.         # Peers
  2321.         hosts = server.get_hosts()
  2322.         geo_ips = server.get_geo_ips()
  2323.         for index, peer in enumerate(peers):
  2324.             if features['dns']:
  2325.                 try:
  2326.                     try:
  2327.                         host = hosts[peer['address']].check()
  2328.                         host_name = host[3][0]
  2329.                     except (IndexError, KeyError):
  2330.                         host_name = "<not resolvable>"
  2331.                 except adns.NotReady:
  2332.                     host_name = "<resolving>"
  2333.                 except adns.Error, msg:
  2334.                     host_name = msg
  2335.  
  2336.             upload_tag = download_tag = line_tag = 0
  2337.             if peer['rateToPeer']:   upload_tag   = curses.A_BOLD
  2338.             if peer['rateToClient']: download_tag = curses.A_BOLD
  2339.  
  2340.             self.pad.move(ypos, 0)
  2341.             # Flags
  2342.             self.pad.addstr("%-6s   " % peer['flagStr'])
  2343.             # Down
  2344.             self.pad.addstr("%5s  " % scale_bytes(peer['rateToClient']), download_tag)
  2345.             # Up
  2346.             self.pad.addstr("%5s " % scale_bytes(peer['rateToPeer']), upload_tag)
  2347.  
  2348.             # Progress
  2349.             if peer['progress'] < 1: self.pad.addstr("%3d%%" % (float(peer['progress'])*100))
  2350.             else: self.pad.addstr("%3d%%" % (float(peer['progress'])*100), curses.A_BOLD)
  2351.  
  2352.             # ETA
  2353.             if peer['progress'] < 1 and peer['download_speed'] > 1024:
  2354.                 self.pad.addstr(" %6s %4s " % \
  2355.                                     ('~' + scale_bytes(peer['download_speed']),
  2356.                                      '~' + scale_time(peer['time_left'])))
  2357.             else:
  2358.                 if peer['progress'] < 1:
  2359.                     self.pad.addstr("  <guessing> ")
  2360.                 else:
  2361.                     self.pad.addstr("             ")
  2362.             # Client
  2363.             self.pad.addstr(self.enc(peer['clientName'].ljust(clientname_width + 1)))
  2364.             # Address:Port
  2365.             self.pad.addstr(peer['address'].rjust(address_width) + \
  2366.                                 ':' + str(peer['port']).ljust(port_width) + ' ')
  2367.             # Country
  2368.             if features['geoip']: self.pad.addstr("  %2s   " % geo_ips[peer['address']])
  2369.             # Host
  2370.             if features['dns']: self.pad.addstr(self.enc(host_name), curses.A_DIM)
  2371.             ypos += 1
  2372.  
  2373.     def draw_trackerlist(self, ypos):
  2374.         top = ypos - 1
  2375.         def addstr(ypos, xpos, *args):
  2376.             if ypos > top and ypos < self.height - 2:
  2377.                 self.pad.addstr(ypos, xpos, *args)
  2378.  
  2379.         tracker_per_page = self.detaillistitems_per_page // self.TRACKER_ITEM_HEIGHT
  2380.         page = self.scrollpos_detaillist // tracker_per_page
  2381.         start = tracker_per_page * page
  2382.         end = tracker_per_page * (page + 1)
  2383.         tlist = self.torrent_details['trackerStats'][start:end]
  2384.  
  2385.         # keep position in range when last tracker gets deleted
  2386.         self.scrollpos_detaillist = min(self.scrollpos_detaillist,
  2387.                                         len(self.torrent_details['trackerStats'])-1)
  2388.         # show newly added tracker when list was empty before
  2389.         if self.torrent_details['trackerStats']:
  2390.             self.scrollpos_detaillist = max(0, self.scrollpos_detaillist)
  2391.  
  2392.         current_tier = -1
  2393.         for index, t in enumerate(tlist):
  2394.             announce_msg_size = scrape_msg_size = 0
  2395.             selected = t == self.torrent_details['trackerStats'][self.scrollpos_detaillist]
  2396.  
  2397.             if current_tier != t['tier']:
  2398.                 current_tier = t['tier']
  2399.  
  2400.                 tiercolor = curses.A_BOLD + curses.A_REVERSE \
  2401.                             if selected else curses.A_REVERSE
  2402.                 addstr(ypos, 0, ("Tier %d" % (current_tier+1)).ljust(self.width), tiercolor)
  2403.                 ypos += 1
  2404.  
  2405.             if selected:
  2406.                 for i in range(4):
  2407.                     addstr(ypos+i, 0, ' ', curses.A_BOLD )
  2408.  
  2409.             addstr(ypos+1, 4,  "Last announce: %s" % timestamp(t['lastAnnounceTime']))
  2410.             addstr(ypos+1, 54, "Last scrape: %s" % timestamp(t['lastScrapeTime']))
  2411.  
  2412.             if t['lastAnnounceSucceeded']:
  2413.                 peers = "%s peer%s" % (num2str(t['lastAnnouncePeerCount']), ('s', '')[t['lastAnnouncePeerCount']==1])
  2414.                 addstr(ypos,   2, t['announce'], curses.A_BOLD + curses.A_UNDERLINE)
  2415.                 addstr(ypos+2, 11, "Result: ")
  2416.                 addstr(ypos+2, 19, "%s received" % peers, curses.A_BOLD)
  2417.             else:
  2418.                 addstr(ypos,   2, t['announce'], curses.A_UNDERLINE)
  2419.                 addstr(ypos+2, 9, "Response:")
  2420.                 announce_msg_size = self.wrap_and_draw_result(top, ypos+2, 19, self.enc(t['lastAnnounceResult']))
  2421.  
  2422.             if t['lastScrapeSucceeded']:
  2423.                 seeds   = "%s seed%s" % (num2str(t['seederCount']), ('s', '')[t['seederCount']==1])
  2424.                 leeches = "%s leech%s" % (num2str(t['leecherCount']), ('es', '')[t['leecherCount']==1])
  2425.                 addstr(ypos+2, 52, "Tracker knows:")
  2426.                 addstr(ypos+2, 67, "%s and %s" % (seeds, leeches), curses.A_BOLD)
  2427.             else:
  2428.                 addstr(ypos+2, 57, "Response:")
  2429.                 scrape_msg_size += self.wrap_and_draw_result(top, ypos+2, 67, t['lastScrapeResult'])
  2430.  
  2431.             ypos += max(announce_msg_size, scrape_msg_size)
  2432.  
  2433.             addstr(ypos+3, 4,  "Next announce: %s" % timestamp(t['nextAnnounceTime']))
  2434.             addstr(ypos+3, 52, "  Next scrape: %s" % timestamp(t['nextScrapeTime']))
  2435.  
  2436.             ypos += 5
  2437.  
  2438.     def wrap_and_draw_result(self, top, ypos, xpos, result):
  2439.         result = wrap(result, 30)
  2440.         i = 0
  2441.         for i, line in enumerate(result):
  2442.             if ypos+i > top and ypos+i < self.height - 2:
  2443.                 self.pad.addstr(ypos+i, xpos, line, curses.A_UNDERLINE)
  2444.         return i
  2445.  
  2446.  
  2447.     def draw_pieces_map(self, ypos):
  2448.         pieces = self.torrent_details['pieces']
  2449.         piece_count = self.torrent_details['pieceCount']
  2450.         margin = len(str(piece_count)) + 2
  2451.  
  2452.         map_width = int(str(self.width-margin-1)[0:-1] + '0')
  2453.         for x in range(10, map_width, 10):
  2454.             self.pad.addstr(ypos, x+margin-1, str(x), curses.A_BOLD)
  2455.  
  2456.         start = self.scrollpos_detaillist * map_width
  2457.         end = min(start + (self.height - ypos - 3) * map_width, piece_count)
  2458.         if end <= start: return
  2459.         block = ord(pieces[start >> 3]) << (start & 7)
  2460.  
  2461.         format = "%%%dd" % (margin - 2)
  2462.         for counter in xrange(start, end):
  2463.             if counter % map_width == 0:
  2464.                 ypos += 1 ; xpos = margin
  2465.                 self.pad.addstr(ypos, 1, format % counter, curses.A_BOLD)
  2466.             else:
  2467.                 xpos += 1
  2468.  
  2469.             if counter & 7 == 0:
  2470.                 block = ord(pieces[counter >> 3])
  2471.             piece = block & 0x80
  2472.             if piece: self.pad.addch(ypos, xpos, ' ', curses.A_REVERSE)
  2473.             else:     self.pad.addch(ypos, xpos, '_')
  2474.             block <<= 1
  2475.  
  2476.         missing_pieces = piece_count - counter - 1
  2477.         if missing_pieces:
  2478.             line = "%d further piece%s" % (missing_pieces, ('','s')[missing_pieces>1])
  2479.             xpos = (self.width - len(line)) / 2
  2480.             self.pad.addstr(self.height-3, xpos, line, curses.A_REVERSE)
  2481.  
  2482.     def draw_details_list(self, ypos, info):
  2483.         key_width = max(map(lambda x: len(x[0]), info))
  2484.         for i in info:
  2485.             self.pad.addstr(ypos, 1, self.enc(i[0].rjust(key_width))) # key
  2486.             # value part may be wrapped if it gets too long
  2487.             for v in i[1:]:
  2488.                 y, x = self.pad.getyx()
  2489.                 if x + len(v) >= self.width:
  2490.                     ypos += 1
  2491.                     self.pad.move(ypos, key_width+1)
  2492.                 self.pad.addstr(self.enc(v))
  2493.             ypos += 1
  2494.         return ypos
  2495.  
  2496.     def next_details(self):
  2497.         if self.details_category_focus >= 4:
  2498.             self.details_category_focus = 0
  2499.         else:
  2500.             self.details_category_focus += 1
  2501.         self.focus_detaillist     = -1
  2502.         self.scrollpos_detaillist = 0
  2503.         self.pad.erase()
  2504.  
  2505.     def prev_details(self):
  2506.         if self.details_category_focus <= 0:
  2507.             self.details_category_focus = 4
  2508.         else:
  2509.             self.details_category_focus -= 1
  2510.         self.pad.erase()
  2511.  
  2512.  
  2513.  
  2514.  
  2515.     def move_up(self, focus, scrollpos, step_size):
  2516.         if focus < 0: focus = -1
  2517.         else:
  2518.             focus -= 1
  2519.             if scrollpos/step_size - focus > 0:
  2520.                 scrollpos -= step_size
  2521.                 scrollpos = max(0, scrollpos)
  2522.             while scrollpos % step_size:
  2523.                 scrollpos -= 1
  2524.         return focus, scrollpos
  2525.  
  2526.     def move_down(self, focus, scrollpos, step_size, elements_per_page, list_height):
  2527.         if focus < list_height - 1:
  2528.             focus += 1
  2529.             if focus+1 - scrollpos/step_size > elements_per_page:
  2530.                 scrollpos += step_size
  2531.         return focus, scrollpos
  2532.  
  2533.     def move_page_up(self, focus, scrollpos, step_size, elements_per_page):
  2534.         for x in range(elements_per_page - 1):
  2535.             focus, scrollpos = self.move_up(focus, scrollpos, step_size)
  2536.         if focus < 0: focus = 0
  2537.         return focus, scrollpos
  2538.  
  2539.     def move_page_down(self, focus, scrollpos, step_size, elements_per_page, list_height):
  2540.         if focus < 0: focus = 0
  2541.         for x in range(elements_per_page - 1):
  2542.             focus, scrollpos = self.move_down(focus, scrollpos, step_size, elements_per_page, list_height)
  2543.         return focus, scrollpos
  2544.  
  2545.     def move_to_top(self):
  2546.         return 0, 0
  2547.  
  2548.     def move_to_end(self, step_size, elements_per_page, list_height):
  2549.         focus     = list_height - 1
  2550.         scrollpos = max(0, (list_height - elements_per_page) * step_size)
  2551.         return focus, scrollpos
  2552.  
  2553.  
  2554.  
  2555.  
  2556.  
  2557.     def draw_stats(self):
  2558.         self.screen.insstr(self.height-1, 0, ' '.center(self.width))
  2559.         self.draw_torrents_stats()
  2560.         self.draw_global_rates()
  2561.  
  2562.     def draw_torrents_stats(self):
  2563.         if self.selected_torrent > -1 and self.details_category_focus == 2:
  2564.             self.screen.insstr((self.height-1), 0,
  2565.                                "%d peer%s connected (" % (self.torrent_details['peersConnected'],
  2566.                                                          ('s','')[self.torrent_details['peersConnected'] == 1]) + \
  2567.                                    "Trackers:%d " % self.torrent_details['peersFrom']['fromTracker'] + \
  2568.                                    "DHT:%d " % self.torrent_details['peersFrom']['fromDht'] + \
  2569.                                    "LTEP:%d " % self.torrent_details['peersFrom']['fromLtep'] + \
  2570.                                    "PEX:%d " % self.torrent_details['peersFrom']['fromPex'] + \
  2571.                                    "Incoming:%d " % self.torrent_details['peersFrom']['fromIncoming'] + \
  2572.                                    "Cache:%d)" % self.torrent_details['peersFrom']['fromCache'],
  2573.                                curses.A_REVERSE)
  2574.         elif vmode_id > -1:
  2575.             self.screen.addstr((self.height-1), 0, "-- VISUAL --")
  2576.         else:
  2577.             self.screen.addstr((self.height-1), 0, "Total%s:" % ('','')[len(self.torrents) == 1]
  2578.                                    )
  2579.             self.screen.addstr("%d " % len(self.torrents))
  2580.  
  2581.             downloading = len(filter(lambda x: x['status']==Transmission.STATUS_DOWNLOAD, self.torrents))
  2582.             seeding = len(filter(lambda x: x['status']==Transmission.STATUS_SEED, self.torrents))
  2583.             paused = self.stats['pausedTorrentCount']
  2584.  
  2585.             self.screen.addstr("Down:")
  2586.             self.screen.addstr("%d " % downloading)
  2587.             self.screen.addstr("Seed:")
  2588.             self.screen.addstr("%d " % seeding)
  2589.             self.screen.addstr("Paused:")
  2590.             self.screen.addstr("%d " % paused)
  2591.  
  2592.             if self.filter_list:
  2593.                 self.screen.addstr("Filter:")
  2594.                 self.screen.addstr("%s%s " % (('','not ')[self.filter_inverse], self.filter_list),
  2595.                                    curses.color_pair(self.colors.id('filter_status'))
  2596.                                    )
  2597.  
  2598.             # show last sort order (if terminal size permits it)
  2599.             curpos_y, curpos_x = self.screen.getyx()
  2600.             if self.sort_orders and self.width - curpos_x > 20:
  2601.                 self.screen.addstr("Sort:")
  2602.                 name = [name[1] for name in self.sort_options if name[0] == self.sort_orders[-1]['name']][0]
  2603.                 name = name.replace('_', '').lower()
  2604.                 curses_tags = curses.color_pair(self.colors.id('filter_status'))
  2605.                 if self.sort_orders[-1]['reverse']:
  2606.                     self.screen.addch(curses.ACS_DARROW, curses_tags)
  2607.                 else:
  2608.                     self.screen.addch(curses.ACS_UARROW, curses_tags)
  2609.                 try:  # 'name' may be too long
  2610.                     self.screen.addstr(name, curses_tags)
  2611.                 except curses.error:
  2612.                     pass
  2613.  
  2614.     def draw_global_rates(self):
  2615.         rates_width = self.rateDownload_width + self.rateUpload_width + 3
  2616.  
  2617.         if self.stats['alt-speed-enabled']:
  2618.             upload_limit   = "/%dK" % self.stats['alt-speed-up']
  2619.             download_limit = "/%dK" % self.stats['alt-speed-down']
  2620.         else:
  2621.             upload_limit   = ('', "/%dK" % self.stats['speed-limit-up'])[self.stats['speed-limit-up-enabled']]
  2622.             download_limit = ('', "/%dK" % self.stats['speed-limit-down'])[self.stats['speed-limit-down-enabled']]
  2623.  
  2624.         limits = {'dn_limit' : download_limit, 'up_limit' : upload_limit}
  2625.         limits_width = len(limits['dn_limit']) + len(limits['up_limit'])
  2626.     curses_tags = curses.color_pair(self.colors.id('filter_status'))
  2627.     curses_tags2 = curses.color_pair(self.colors.id('title_download'))
  2628.  
  2629.         if self.stats['alt-speed-enabled']:
  2630.             self.screen.move(self.height-1, self.width-rates_width - limits_width - len('Turtle mode '))
  2631.             self.screen.addstr('Turtle mode')
  2632.             self.screen.addch(' ')
  2633.  
  2634.         self.screen.move(self.height - 1, self.width - rates_width - limits_width)
  2635.         self.screen.addch(curses.ACS_DARROW)
  2636.         self.screen.addstr(scale_bytes(self.stats['downloadSpeed']).rjust(self.rateDownload_width),
  2637.                            curses_tags2
  2638.                           )
  2639.         self.screen.addstr(limits['dn_limit'])
  2640.         self.screen.addch(' ')
  2641.         self.screen.addch(curses.ACS_UARROW)
  2642.         self.screen.insstr(limits['up_limit'])
  2643.         self.screen.insstr(scale_bytes(self.stats['uploadSpeed']).rjust(self.rateUpload_width),
  2644.                            curses_tags
  2645.                            )
  2646.  
  2647.  
  2648.  
  2649.     def draw_title_bar(self):
  2650.         self.screen.insstr(0, 0, ' '.center(self.width))
  2651.         self.draw_connection_status()
  2652.         self.draw_quick_help()
  2653.     def draw_connection_status(self):
  2654.         status = ""
  2655.         if cmd_args.DEBUG:
  2656.             status = "%d x %d " % (self.width, self.height) + status
  2657.         self.screen.addstr(0, 0, self.enc(status))
  2658.  
  2659.     def draw_quick_help(self):
  2660.         help = [('?','Controls')]
  2661.  
  2662.         if self.selected_torrent == -1:
  2663.             if self.focus >= 0:
  2664.                 help = [('enter','Details'), ('p','Pause/Unpause'), ('r','Remove'), ('v','Verify')]
  2665.             else:
  2666.                 help = [('/','Search'), ('f','Filter'), ('s','Sort')] + help + [('o','Options'), ('q','Quit')]
  2667.         else:
  2668.             help = [('Move with','cursor keys'), ('q','Back to List')]
  2669.             if self.details_category_focus == 1 and self.focus_detaillist > -1:
  2670.                 help = [('enter', 'Open File'),
  2671.                         ('space','(De)Select File'),
  2672.                         ('V','Visually Select Files'),
  2673.                         ('left/right','De-/Increase Priority'),
  2674.                         ('escape','Unfocus/-select')] + help
  2675.             elif self.details_category_focus == 2:
  2676.                 help = [('F1/?','Explain flags')] + help
  2677.             elif self.details_category_focus == 3:
  2678.                 help = [('a','Add Tracker'),('r','Remove Tracker')] + help
  2679.  
  2680.         line = ' '.join(map(lambda x: "%s %s" % (x[0], x[1]), help))
  2681.         line = line[0:self.width]
  2682.         self.screen.insstr(0, self.width-len(line), line)
  2683.  
  2684.  
  2685.     def list_key_bindings(self):
  2686.         title = 'Help Menu'
  2687.         message = "           F1/?  Show this help\n" + \
  2688.                   "            u/d  Adjust maximum global up-/download rate\n" + \
  2689.                   "            U/D  Adjust maximum up-/download rate for focused torrent\n" + \
  2690.                   "              L  Set seed ratio limit for focused torrent\n" + \
  2691.                   "            +/-  Adjust bandwidth priority for focused torrent\n" + \
  2692.                   "              p  Pause/Unpause torrent\n" + \
  2693.                   "              P  Pause/Unpause all torrents\n" + \
  2694.                   "              N  Start torrent now\n" + \
  2695.                   "            v/y  Verify torrent\n" + \
  2696.                   "              m  Move torrent\n" + \
  2697.                   "              n  Reannounce torrent\n" + \
  2698.                   "              a  Add torrent\n" + \
  2699.                   "              A  Add torrent by hash\n" + \
  2700.                   "          Del/r  Remove torrent and keep content\n" + \
  2701.                   "    Shift+Del/R  Remove torrent and delete content\n"
  2702.  
  2703.         # Queue was implemented in Transmission v2.4
  2704.         if server.get_rpc_version() >= 14 and self.details_category_focus != 1:
  2705.             message += "            J/K  Move focused torrent in queue up/down\n" + \
  2706.                        " Shift+Lft/Rght  Move focused torrent in queue up/down by 10\n" + \
  2707.                        " Shift+Home/End  Move focused torrent to top/bottom of queue\n"
  2708.         else:
  2709.             message += "            J/K  Jump to next/previous directory\n"
  2710.         # Torrent list
  2711.         if self.selected_torrent == -1:
  2712.             message += "              /  Search in torrent list\n" + \
  2713.                        "              f  Filter torrent list\n" + \
  2714.                        "              s  Sort torrent list\n" \
  2715.                        "    Enter/Right  View torrent's details\n" + \
  2716.                        "              o  Configuration options\n" + \
  2717.                        "              t  Toggle turtle mode\n" + \
  2718.                        "              C  Toggle compact list mode\n" + \
  2719.                        "            Esc  Unfocus\n" + \
  2720.                        "              q  Quit"
  2721.         else:
  2722.             # Peer list
  2723.             if self.details_category_focus == 2:
  2724.                 title = 'Peer status flags'
  2725.                 message = " O  Optimistic unchoke\n" + \
  2726.                           " D  Downloading from this peer\n" + \
  2727.                           " d  We would download from this peer if they'd let us\n" + \
  2728.                           " U  Uploading to peer\n" + \
  2729.                           " u  We would upload to this peer if they'd ask\n" + \
  2730.                           " K  Peer has unchoked us, but we're not interested\n" + \
  2731.                           " ?  We unchoked this peer, but they're not interested\n" + \
  2732.                           " E  Encrypted Connection\n" + \
  2733.                           " H  Peer was discovered through DHT\n" + \
  2734.                           " X  Peer was discovered through Peer Exchange (PEX)\n" + \
  2735.                           " I  Peer is an incoming connection\n" + \
  2736.                           " T  Peer is connected via uTP"
  2737.             else:
  2738.                 # Viewing torrent details
  2739.                 message += "              o  Jump to overview\n" + \
  2740.                            "              f  Jump to file list\n" + \
  2741.                            "              e  Jump to peer list\n" + \
  2742.                            "              t  Jump to tracker information\n" + \
  2743.                            "      Tab/Right  Jump to next view\n" + \
  2744.                            " Shift+Tab/Left  Jump to previous view\n"
  2745.                 if self.details_category_focus == 1:  # files
  2746.                     if self.focus_detaillist > -1:
  2747.                         message += "     Left/Right  Decrease/Increase file priority\n"
  2748.                     message += "        Up/Down  Select file\n" + \
  2749.                                "          Space  Select/Deselect focused file\n" + \
  2750.                                "              a  Select/Deselect all files\n" + \
  2751.                                "              A  Select/Deselect directory\n" + \
  2752.                                "              V  Visually select files\n" + \
  2753.                                "            Esc  Unfocus+Unselect or Back to torrent list\n" + \
  2754.                                "    q/Backspace  Back to torrent list"
  2755.                 else:
  2756.                     message += "q/Backspace/Esc  Back to torrent list"
  2757.  
  2758.         width  = max(map(lambda x: len(x), message.split("\n"))) + 4
  2759.         width  = min(self.width, width)
  2760.         height = min(self.height, message.count("\n")+3)
  2761.         win = self.window(height, width, message=message, title=title)
  2762.         while True:
  2763.             if win.getch() >= 0: return
  2764.  
  2765.  
  2766.     def window(self, height, width, message='', title=''):
  2767.         height = min(self.height, height)
  2768.         width  = min(self.width, width)
  2769.         ypos = int( (self.height - height) / 2 )
  2770.         xpos = int( (self.width  - width) / 2 )
  2771.         win = curses.newwin(height, width, ypos, xpos)
  2772.         win.box()
  2773.         win.bkgd(' ', curses.A_REVERSE + curses.A_BOLD)
  2774.  
  2775.         if width >= 20:
  2776.             win.addch(height-1, width-19, curses.ACS_RTEE)
  2777.             win.addstr(height-1, width-18, " Close with Esc ")
  2778.             win.addch(height-1, width-2, curses.ACS_LTEE)
  2779.  
  2780.         if width >= (len(title) + 6) and title != '':
  2781.             win.addch(0, 1, curses.ACS_RTEE)
  2782.             win.addstr(0, 2, " " + title + " ")
  2783.             win.addch(0, len(title) + 4 , curses.ACS_LTEE)
  2784.  
  2785.         ypos = 1
  2786.         for line in message.split("\n"):
  2787.             if len_columns(line) > width:
  2788.                 line = ljust_columns(line, width-7) + '...'
  2789.  
  2790.             if ypos < height - 1:  # ypos == height-1 is frame border
  2791.                 win.addstr(ypos, 2, self.enc(line))
  2792.                 ypos += 1
  2793.             else:
  2794.                 # Do not write outside of frame border
  2795.                 win.addstr(ypos, 2, " More... ")
  2796.                 break
  2797.         return win
  2798.  
  2799.  
  2800.     def dialog_ok(self, message):
  2801.         height = 3 + message.count("\n")
  2802.         width  = max(max(map(lambda x: len_columns(x), message.split("\n"))), 40) + 4
  2803.         win = self.window(height, width, message=message)
  2804.         while True:
  2805.             if win.getch() >= 0: return
  2806.  
  2807.     def dialog_yesno(self, message, important=False):
  2808.         height = 5 + message.count("\n")
  2809.         width  = max(len_columns(message), 8) + 4
  2810.         win = self.window(height, width, message=message)
  2811.         win.keypad(True)
  2812.  
  2813.         if important:
  2814.             win.bkgd(' ', curses.color_pair(self.colors.id('dialog_important'))
  2815.                           + curses.A_REVERSE)
  2816.  
  2817.         focus_tags   = curses.color_pair(self.colors.id('button_focused'))
  2818.         unfocus_tags = 0
  2819.  
  2820.         input = False
  2821.         while True:
  2822.             win.move(height-2, (width/2)-4)
  2823.             if input:
  2824.                 win.addstr('Y',  focus_tags + curses.A_UNDERLINE)
  2825.                 win.addstr('es', focus_tags)
  2826.                 win.addstr('   ')
  2827.                 win.addstr('N',  curses.A_UNDERLINE)
  2828.                 win.addstr('o')
  2829.             else:
  2830.                 win.addstr('Y', curses.A_UNDERLINE)
  2831.                 win.addstr('es')
  2832.                 win.addstr('   ')
  2833.                 win.addstr('N',  focus_tags + curses.A_UNDERLINE)
  2834.                 win.addstr('o', focus_tags)
  2835.  
  2836.             c = win.getch()
  2837.             if c == ord('y'):
  2838.                 return True
  2839.             elif c == ord('n'):
  2840.                 return False
  2841.             elif c == ord("\t"):
  2842.                 input = not input
  2843.             elif c == curses.KEY_LEFT or c == ord('h'):
  2844.                 input = True
  2845.             elif c == curses.KEY_RIGHT or c == ord('l'):
  2846.                 input = False
  2847.             elif c == ord("\n") or c == ord(' '):
  2848.                 return input
  2849.             elif c == 27 or c == curses.KEY_BREAK:
  2850.                 return -1
  2851.  
  2852.  
  2853.     # tab_complete values:
  2854.     #             'files': complete with any files/directories
  2855.     #              'dirs': complete only with directories
  2856.     #      'torrent_list': complete with names from the torrent list
  2857.     #     any false value: do not complete
  2858.     def dialog_input_text(self, message, input='', on_change=None, on_enter=None, tab_complete=None):
  2859.         width  = self.width - 4
  2860.         textwidth = self.width - 8
  2861.         height = message.count("\n") + 4
  2862.  
  2863.         win = self.window(height, width, message=message)
  2864.         win.keypad(True)
  2865.         show_cursor()
  2866.         if not isinstance(input, unicode):
  2867.             input = unicode(input, ENCODING)
  2868.         index = len(input)
  2869.         while True:
  2870.             # Cut the text into pages, each as long as the text field
  2871.             # The current page is determined by index position
  2872.             page = index // textwidth
  2873.             displaytext = input[textwidth*page:textwidth*(page + 1)]
  2874.             displayindex = index - textwidth*page
  2875.  
  2876.             color = (curses.color_pair(self.colors.id('dialog_important')) if self.highlight_dialog
  2877.                      else curses.color_pair(self.colors.id('dialog')))
  2878.             win.addstr(height - 2, 2, self.enc(displaytext.ljust(textwidth)), color)
  2879.             win.move(height - 2, displayindex + 2)
  2880.             c = win.getch()
  2881.             if c == 27 or c == curses.KEY_BREAK:
  2882.                 hide_cursor()
  2883.                 return ''
  2884.             elif index < len(input) and ( c == curses.KEY_RIGHT or c == curses.ascii.ctrl(ord('f')) ):
  2885.                 index += 1
  2886.             elif index > 0 and ( c == curses.KEY_LEFT or c == curses.ascii.ctrl(ord('b')) ):
  2887.                 index -= 1
  2888.             elif (c == curses.KEY_BACKSPACE or c == 127) and index > 0:
  2889.                 input = input[:index - 1] + (index < len(input) and input[index:] or '')
  2890.                 index -= 1
  2891.                 if on_change: on_change(input)
  2892.             elif index < len(input) and ( c == curses.KEY_DC or c == curses.ascii.ctrl(ord('d')) ):
  2893.                 input = input[:index] + input[index + 1:]
  2894.                 if on_change: on_change(input)
  2895.             elif index < len(input) and c == curses.ascii.ctrl(ord('k')):
  2896.                 input = input[:index]
  2897.                 if on_change: on_change(input)
  2898.             elif c == curses.ascii.ctrl(ord('u')):
  2899.                 # Delete from cursor until beginning of line
  2900.                 input = input[index:]
  2901.                 index = 0
  2902.                 if on_change: on_change(input)
  2903.             elif c == curses.KEY_HOME or c == curses.ascii.ctrl(ord('a')):
  2904.                 index = 0
  2905.             elif c == curses.KEY_END or c == curses.ascii.ctrl(ord('e')):
  2906.                 index = len(input)
  2907.             elif c == ord('\n'):
  2908.                 if on_enter:
  2909.                     on_enter(input)
  2910.                 else:
  2911.                     hide_cursor()
  2912.                     return input
  2913.             elif c >= 32 and c < 127:
  2914.                 input = input[:index] + unicode(chr(c), ENCODING) + (index < len(input) and input[index:] or '')
  2915.                 index += 1
  2916.                 if on_change: on_change(input)
  2917.             elif c == ord('\t') and tab_complete:
  2918.                 possible_choices = [];
  2919.                 if tab_complete in ('files', 'dirs'):
  2920.                     (dirname, filename) = os.path.split(tilde2homedir(input))
  2921.                     if not dirname:
  2922.                         dirname = unicode(os.getcwd())
  2923.                     try:
  2924.                         possible_choices = [ os.path.join(dirname, choice) for choice in os.listdir(dirname)
  2925.                                              if choice.startswith(filename) ]
  2926.                     except OSError:
  2927.                         continue;
  2928.                     if tab_complete == 'dirs':
  2929.                         possible_choices = [ d for d in possible_choices
  2930.                                              if os.path.isdir(d) ]
  2931.                 elif tab_complete == 'torrent_list':
  2932.                     possible_choices = [ t['name'] for t in self.torrents
  2933.                                          if t['name'].startswith(input) ]
  2934.                 if(possible_choices):
  2935.                     input = os.path.commonprefix(possible_choices)
  2936.                     if tab_complete in ('files', 'dirs'):
  2937.                         if len(possible_choices) == 1 and os.path.isdir(input) and input.endswith(os.sep) == False:
  2938.                             input += os.sep
  2939.                         input = homedir2tilde(input)
  2940.                     index = len(input)
  2941.                     if on_change: on_change(input);
  2942.             if on_change: win.redrawwin()
  2943.  
  2944.     def dialog_search_torrentlist(self, c):
  2945.         self.dialog_input_text('Search torrent by title:',
  2946.                                on_change=self.draw_torrent_list,
  2947.                                on_enter=self.increment_search,
  2948.                                tab_complete = 'torrent_list')
  2949.  
  2950.     def increment_search(self, input):
  2951.         self.search_focus += 1
  2952.         self.draw_torrent_list(input)
  2953.  
  2954.  
  2955.     def dialog_input_number(self, message, current_value,
  2956.                             cursorkeys=True, floating_point=False, allow_empty=False,
  2957.                             allow_zero=True, allow_negative_one=True):
  2958.         if not allow_zero:
  2959.             allow_negative_one = False
  2960.  
  2961.         width  = max(max(map(lambda x: len(x), message.split("\n"))), 40) + 4
  2962.         width  = min(self.width, width)
  2963.         height = message.count("\n") + (4,6)[cursorkeys]
  2964.  
  2965.         show_cursor()
  2966.         win = self.window(height, width, message=message)
  2967.         win.keypad(True)
  2968.         input = str(current_value)
  2969.         if cursorkeys:
  2970.             if floating_point:
  2971.                 bigstep   = 1
  2972.                 smallstep = 0.1
  2973.             else:
  2974.                 bigstep   = 100
  2975.                 smallstep = 10
  2976.             win.addstr(height-4, 2, ("   up/down +/- %-3s" % bigstep).rjust(width-4))
  2977.             win.addstr(height-3, 2, ("left/right +/- %3s" % smallstep).rjust(width-4))
  2978.             if allow_negative_one:
  2979.                 win.addstr(height-3, 2, "-1 means unlimited")
  2980.             if allow_empty:
  2981.                 win.addstr(height-4, 2, "leave empty for default")
  2982.  
  2983.         while True:
  2984.             win.addstr(height-2, 2, input.ljust(width-4), curses.color_pair(self.colors.id('dialog')))
  2985.             win.move(height - 2, len(input) + 2)
  2986.             c = win.getch()
  2987.             if c == 27 or c == ord('q') or c == curses.KEY_BREAK:
  2988.                 hide_cursor()
  2989.                 return -128
  2990.             elif c == ord("\n"):
  2991.                 try:
  2992.                     if allow_empty and len(input) <= 0:
  2993.                         hide_cursor()
  2994.                         return -2
  2995.                     elif floating_point:
  2996.                         hide_cursor()
  2997.                         return float(input)
  2998.                     else:
  2999.                         hide_cursor()
  3000.                         return int(input)
  3001.                 except ValueError:
  3002.                     hide_cursor()
  3003.                     return -1
  3004.  
  3005.             elif c == curses.KEY_BACKSPACE or c == curses.KEY_DC or c == 127 or c == 8:
  3006.                 input = input[:-1]
  3007.             elif len(input) >= width-5:
  3008.                 curses.beep()
  3009.             elif c >= ord('1') and c <= ord('9'):
  3010.                 input += chr(c)
  3011.             elif allow_zero and c == ord('0') and input != '-' and not input.startswith('0'):
  3012.                 input += chr(c)
  3013.             elif allow_negative_one and c == ord('-') and len(input) == 0:
  3014.                 input += chr(c)
  3015.             elif floating_point and c == ord('.') and not '.' in input:
  3016.                 input += chr(c)
  3017.  
  3018.             elif cursorkeys and c != -1:
  3019.                 try:
  3020.                     if input == '': input = 0
  3021.                     if floating_point: number = float(input)
  3022.                     else:              number = int(input)
  3023.                     if c == curses.KEY_LEFT or c == ord('h'):    number -= smallstep
  3024.                     elif c == curses.KEY_RIGHT or c == ord('l'): number += smallstep
  3025.                     elif c == curses.KEY_DOWN or c == ord('j'):  number -= bigstep
  3026.                     elif c == curses.KEY_UP or c == ord('k'):    number += bigstep
  3027.                     if not allow_zero and number <= 0:
  3028.                         number = 1
  3029.                     elif not allow_negative_one and number < 0:
  3030.                         number = 0
  3031.                     elif number < 0:  # input like -0.6 isn't useful
  3032.                         number = -1
  3033.                     input = str(number)
  3034.                 except ValueError:
  3035.                     pass
  3036.  
  3037.     def dialog_menu(self, title, options, focus=1):
  3038.         height = len(options) + 2
  3039.         width  = max(max(map(lambda x: len(x[1])+3, options)), len(title)+3)
  3040.         win = self.window(height, width)
  3041.  
  3042.         win.addstr(0,1, title)
  3043.         win.keypad(True)
  3044.  
  3045.         old_focus = focus
  3046.         while True:
  3047.             keymap = self.dialog_list_menu_options(win, width, options, focus)
  3048.             c = win.getch()
  3049.  
  3050.             if c > 96 and c < 123 and chr(c) in keymap:
  3051.                 return options[keymap[chr(c)]][0]
  3052.             elif c == 27 or c == ord('q'):
  3053.                 return -128
  3054.             elif c == ord("\n"):
  3055.                 return options[focus-1][0]
  3056.             elif c == curses.KEY_DOWN or c == ord('j') or c == curses.ascii.ctrl(ord('n')):
  3057.                 focus += 1
  3058.                 if focus > len(options): focus = 1
  3059.             elif c == curses.KEY_UP or c == ord('k') or c == curses.ascii.ctrl(ord('p')):
  3060.                 focus -= 1
  3061.                 if focus < 1: focus = len(options)
  3062.             elif c == curses.KEY_HOME or c == ord('g'):
  3063.                 focus = 1
  3064.             elif c == curses.KEY_END or c == ord('G'):
  3065.                 focus = len(options)
  3066.  
  3067.     def dialog_list_menu_options(self, win, width, options, focus):
  3068.         keys = dict()
  3069.         i = 1
  3070.         for option in options:
  3071.             title = option[1].split('_')
  3072.             if i == focus: tag = curses.color_pair(self.colors.id('dialog'))
  3073.             else:          tag = 0
  3074.             win.addstr(i,2, title[0], tag)
  3075.             win.addstr(title[1][0], tag + curses.A_UNDERLINE)
  3076.             win.addstr(title[1][1:], tag)
  3077.             win.addstr(''.ljust(width - len(option[1]) - 3), tag)
  3078.  
  3079.             keys[title[1][0].lower()] = i-1
  3080.             i+=1
  3081.         return keys
  3082.  
  3083.     def draw_options_dialog(self):
  3084.         enc_options = [('required','_required'), ('preferred','_preferred'), ('tolerated','_tolerated')]
  3085.         seed_ratio = self.stats['seedRatioLimit']
  3086.         while True:
  3087.             options = []
  3088.             options.append(('Peer _Port', "%d" % self.stats['peer-port']))
  3089.             options.append(('UP_nP/NAT-PMP', ('disabled','enabled ')[self.stats['port-forwarding-enabled']]))
  3090.             options.append(('Peer E_xchange', ('disabled','enabled ')[self.stats['pex-enabled']]))
  3091.             options.append(('_Distributed Hash Table', ('disabled','enabled ')[self.stats['dht-enabled']]))
  3092.             options.append(('_Local Peer Discovery', ('disabled','enabled ')[self.stats['lpd-enabled']]))
  3093.             options.append(('Protocol En_cryption', "%s" % self.stats['encryption']))
  3094.             # uTP support was added in Transmission v2.3
  3095.             if server.get_rpc_version() >= 13:
  3096.                 options.append(('_Micro Transport Protocol', ('disabled','enabled')[self.stats['utp-enabled']]))
  3097.             options.append(('_Global Peer Limit', "%d" % self.stats['peer-limit-global']))
  3098.             options.append(('Peer Limit per _Torrent', "%d" % self.stats['peer-limit-per-torrent']))
  3099.             options.append(('T_urtle Mode UL Limit', "%dK" % self.stats['alt-speed-up']))
  3100.             options.append(('Tu_rtle Mode DL Limit', "%dK" % self.stats['alt-speed-down']))
  3101.             options.append(('_Seed Ratio Limit', "%s" % ('unlimited',self.stats['seedRatioLimit'])[self.stats['seedRatioLimited']]))
  3102.             # queue was implemented in Transmission v2.4
  3103.             if server.get_rpc_version() >= 14:
  3104.                 options.append(('Do_wnload Queue Size', "%s" % ('disabled',self.stats['download-queue-size'])[self.stats['download-queue-enabled']]))
  3105.                 options.append(('S_eed Queue Size', "%s" % ('disabled',self.stats['seed-queue-size'])[self.stats['seed-queue-enabled']]))
  3106.             options.append(('Title is Progress _Bar', ('no','yes')[self.torrentname_is_progressbar]))
  3107.             options.append(('Blan_k lines in non-compact', ('no','yes')[self.blank_lines]))
  3108.             options.append(('File _Viewer', "%s" % self.file_viewer))
  3109.  
  3110.  
  3111.             max_len = max([sum([len(re.sub('_', '', x)) for x in y[0]]) for y in options])
  3112.             win_width = min(max(len(self.file_viewer)+5, 15), self.width+max_len)
  3113.             win = self.window(len(options)+2, max_len+win_width, '', "Global Options")
  3114.  
  3115.             line_num = 1
  3116.             for option in options:
  3117.                 parts = re.split('_', option[0])
  3118.                 parts_len = sum([len(x) for x in parts])
  3119.  
  3120.                 win.addstr(line_num, max_len-parts_len+2, parts.pop(0))
  3121.                 for part in parts:
  3122.                     win.addstr(part[0], curses.A_UNDERLINE)
  3123.                     win.addstr(part[1:] + ': ' + option[1])
  3124.                 line_num += 1
  3125.  
  3126.             c = win.getch()
  3127.             if c == 27 or c == ord('q') or c == ord("\n"):
  3128.                 return
  3129.             elif c == ord('p'):
  3130.                 port = self.dialog_input_number("Port for incoming connections",
  3131.                                                 self.stats['peer-port'],
  3132.                                                 cursorkeys=False)
  3133.                 if port >= 0 and port <= 65535:
  3134.                     server.set_option('peer-port', port)
  3135.                 elif port != -128:  # user hit ESC
  3136.                     self.dialog_ok('Port must be in the range of 0 - 65535')
  3137.             elif c == ord('n'):
  3138.                 server.set_option('port-forwarding-enabled',
  3139.                                        (1,0)[self.stats['port-forwarding-enabled']])
  3140.             elif c == ord('x'):
  3141.                 server.set_option('pex-enabled', (1,0)[self.stats['pex-enabled']])
  3142.             elif c == ord('d'):
  3143.                 server.set_option('dht-enabled', (1,0)[self.stats['dht-enabled']])
  3144.             elif c == ord('l'):
  3145.                 server.set_option('lpd-enabled', (1,0)[self.stats['lpd-enabled']])
  3146.             # uTP support was added in Transmission v2.3
  3147.             elif c == ord('m') and server.get_rpc_version() >= 13:
  3148.                 server.set_option('utp-enabled', (1,0)[self.stats['utp-enabled']])
  3149.             elif c == ord('g'):
  3150.                 limit = self.dialog_input_number("Maximum number of connected peers",
  3151.                                                  self.stats['peer-limit-global'],
  3152.                                                  allow_negative_one=False)
  3153.                 if limit >= 0:
  3154.                     server.set_option('peer-limit-global', limit)
  3155.             elif c == ord('t'):
  3156.                 limit = self.dialog_input_number("Maximum number of connected peers per torrent",
  3157.                                                  self.stats['peer-limit-per-torrent'],
  3158.                                                  allow_negative_one=False)
  3159.                 if limit >= 0:
  3160.                     server.set_option('peer-limit-per-torrent', limit)
  3161.             elif c == ord('s'):
  3162.                 limit = self.dialog_input_number('Stop seeding with upload/download ratio',
  3163.                                                  (-1,self.stats['seedRatioLimit'])[self.stats['seedRatioLimited']],
  3164.                                                  floating_point=True)
  3165.                 if limit >= 0:
  3166.                     server.set_option('seedRatioLimit', limit)
  3167.                     server.set_option('seedRatioLimited', True)
  3168.                 elif limit < 0 and limit != -128:
  3169.                     server.set_option('seedRatioLimited', False)
  3170.             elif c == ord('c'):
  3171.                 choice = self.dialog_menu('Encryption', enc_options,
  3172.                                           map(lambda x: x[0]==self.stats['encryption'], enc_options).index(True)+1)
  3173.                 if choice != -128:
  3174.                     server.set_option('encryption', choice)
  3175.             elif c == ord('u'):
  3176.                 limit = self.dialog_input_number('Upload limit for Turtle Mode in kilobytes per second',
  3177.                                                  self.stats['alt-speed-up'],
  3178.                                                  allow_negative_one=False)
  3179.                 if limit != -128:
  3180.                     server.set_option('alt-speed-up', limit)
  3181.             elif c == ord('r'):
  3182.                 limit = self.dialog_input_number('Download limit for Turtle Mode in kilobytes per second',
  3183.                                                  self.stats['alt-speed-down'],
  3184.                                                  allow_negative_one=False)
  3185.                 if limit != -128:
  3186.                     server.set_option('alt-speed-down', limit)
  3187.             elif c == ord('b'):
  3188.                 self.torrentname_is_progressbar = not self.torrentname_is_progressbar
  3189.             # Queue was implemmented in Transmission v2.4
  3190.             elif c == ord('w') and server.get_rpc_version() >= 14:
  3191.                 queue_size = self.dialog_input_number('Download Queue size',
  3192.                                                       (0, self.stats['download-queue-size'])[self.stats['download-queue-enabled']],
  3193.                                                       allow_negative_one = False)
  3194.                 if queue_size != -128:
  3195.                     if queue_size == 0:
  3196.                         server.set_option('download-queue-enabled', False)
  3197.                     elif queue_size > 0:
  3198.                         if not self.stats['download-queue-enabled']:
  3199.                             server.set_option('download-queue-enabled', True)
  3200.                         server.set_option('download-queue-size', queue_size)
  3201.             # Queue was implemmented in Transmission v2.4
  3202.             elif c == ord('e') and server.get_rpc_version() >= 14:
  3203.                 queue_size = self.dialog_input_number('Seed Queue size',
  3204.                                                       (0, self.stats['seed-queue-size'])[self.stats['seed-queue-enabled']],
  3205.                                                       allow_negative_one = False)
  3206.                 if queue_size != -128:
  3207.                     if queue_size == 0:
  3208.                         server.set_option('seed-queue-enabled', False)
  3209.                     elif queue_size > 0:
  3210.                         if not self.stats['seed-queue-enabled']:
  3211.                             server.set_option('seed-queue-enabled', True)
  3212.                         server.set_option('seed-queue-size', queue_size)
  3213.  
  3214.             elif c == ord('k'):
  3215.                 self.blank_lines = not self.blank_lines
  3216.  
  3217.             elif c == ord('v'):
  3218.                 viewer = self.dialog_input_text('File Viewer\nExample: xdg-viewer %s', self.file_viewer)
  3219.                 if viewer:
  3220.                     config.set('Misc', 'file_viewer', viewer.replace('%s','%%s'))
  3221.                     self.file_viewer=viewer
  3222.  
  3223.             self.draw_torrent_list()
  3224.  
  3225. # End of class Interface
  3226.  
  3227.  
  3228.  
  3229. def percent(full, part):
  3230.     try: percent = 100/(float(full) / float(part))
  3231.     except ZeroDivisionError: percent = 0.0
  3232.     return percent
  3233.  
  3234.  
  3235. def scale_time(seconds, type='short'):
  3236.     minute_in_sec = float(60)
  3237.     hour_in_sec   = float(3600)
  3238.     day_in_sec    = float(86400)
  3239.     month_in_sec  = 27.321661 * day_in_sec # from wikipedia
  3240.     year_in_sec   = 365.25    * day_in_sec # from wikipedia
  3241.  
  3242.     if seconds < 0:
  3243.         return ('?', 'some time')[type=='long']
  3244.  
  3245.     elif seconds < minute_in_sec:
  3246.         if type == 'long':
  3247.             if seconds < 5:
  3248.                 return 'now'
  3249.             else:
  3250.                 return "%d second%s" % (seconds, ('', 's')[seconds>1])
  3251.         else:
  3252.             return "%ds" % seconds
  3253.  
  3254.     elif seconds < hour_in_sec:
  3255.         minutes = round(seconds / minute_in_sec, 0)
  3256.         if type == 'long':
  3257.             return "%d minute%s" % (minutes, ('', 's')[minutes>1])
  3258.         else:
  3259.             return "%dm" % minutes
  3260.  
  3261.     elif seconds < day_in_sec:
  3262.         hours = round(seconds / hour_in_sec, 0)
  3263.         if type == 'long':
  3264.             return "%d hour%s" % (hours, ('', 's')[hours>1])
  3265.         else:
  3266.             return "%dh" % hours
  3267.  
  3268.     elif seconds < month_in_sec:
  3269.         days = round(seconds / day_in_sec, 0)
  3270.         if type == 'long':
  3271.             return "%d day%s" % (days, ('', 's')[days>1])
  3272.         else:
  3273.             return "%dd" % days
  3274.  
  3275.     elif seconds < year_in_sec:
  3276.         months = round(seconds / month_in_sec, 0)
  3277.         if type == 'long':
  3278.             return "%d month%s" % (months, ('', 's')[months>1])
  3279.         else:
  3280.             return "%dM" % months
  3281.  
  3282.     else:
  3283.         years = round(seconds / year_in_sec, 0)
  3284.         if type == 'long':
  3285.             return "%d year%s" % (years, ('', 's')[years>1])
  3286.         else:
  3287.             return "%dy" % years
  3288.  
  3289.  
  3290. def timestamp(timestamp, format="%x %X"):
  3291.     if timestamp < 1:
  3292.         return 'never'
  3293.  
  3294.     if timestamp > 2147483647:  # Max value of 32bit signed integer (2^31-1)
  3295.         # Timedelta objects do not fail on timestamps
  3296.         # resulting in a date later than 2038
  3297.         date = (datetime.datetime.fromtimestamp(0) +
  3298.                 datetime.timedelta(seconds=timestamp))
  3299.         timeobj = date.timetuple()
  3300.     else:
  3301.         timeobj = time.localtime(timestamp)
  3302.  
  3303.     absolute = time.strftime(format, timeobj)
  3304.     if timestamp > time.time():
  3305.         relative = 'in ' + scale_time(int(timestamp - time.time()), 'long')
  3306.     else:
  3307.         relative = scale_time(int(time.time() - timestamp), 'long') + ' ago'
  3308.  
  3309.     if relative.startswith('now') or relative.endswith('now'):
  3310.         relative = 'now'
  3311.     return "%s (%s)" % (absolute, relative)
  3312.  
  3313.  
  3314. def scale_bytes(bytes, type='short'):
  3315.     if bytes >= 1073741824:
  3316.         scaled_bytes = round((bytes / 1073741824.0), 2)
  3317.         unit = 'G'
  3318.     elif bytes >= 1048576:
  3319.         scaled_bytes = round((bytes / 1048576.0), 1)
  3320.         if scaled_bytes >= 100:
  3321.             scaled_bytes = int(scaled_bytes)
  3322.         unit = 'M'
  3323.     elif bytes >= 1024:
  3324.         scaled_bytes = int(bytes / 1024)
  3325.         unit = 'K'
  3326.     else:
  3327.         scaled_bytes = round((bytes / 1024.0), 1)
  3328.         unit = 'K'
  3329.  
  3330.  
  3331.     # handle 0 bytes special
  3332.     if bytes == 0 and type == 'long':
  3333.         return 'nothing'
  3334.  
  3335.     # convert to integer if .0
  3336.     if int(scaled_bytes) == float(scaled_bytes):
  3337.         scaled_bytes = str(int(scaled_bytes))
  3338.     else:
  3339.         scaled_bytes = str(scaled_bytes).rstrip('0')
  3340.  
  3341.     if type == 'long':
  3342.         return num2str(bytes) + ' [' + scaled_bytes + unit + ']'
  3343.     else:
  3344.         return scaled_bytes + unit
  3345.  
  3346.  
  3347. def homedir2tilde(path):
  3348.     return re.sub(r'^'+os.environ['HOME'], '~', path)
  3349. def tilde2homedir(path):
  3350.     return re.sub(r'^~', os.environ['HOME'], path)
  3351.  
  3352. def html2text(str):
  3353.     str = re.sub(r'</h\d+>', "\n", str)
  3354.     str = re.sub(r'</p>', ' ', str)
  3355.     str = re.sub(r'<[^>]*?>', '', str)
  3356.     return str
  3357.  
  3358. def hide_cursor():
  3359.     try: curses.curs_set(0)   # hide cursor if possible
  3360.     except curses.error: pass # some terminals seem to have problems with that
  3361. def show_cursor():
  3362.     try: curses.curs_set(1)
  3363.     except curses.error: pass
  3364.  
  3365. def wrap_multiline(text, width, initial_indent='', subsequent_indent=None):
  3366.     if subsequent_indent is None:
  3367.         subsequent_indent = ' ' * len(initial_indent)
  3368.     for line in text.splitlines():
  3369.         # this is required because wrap() strips empty lines
  3370.         if not line.strip():
  3371.             yield line
  3372.             continue
  3373.         for line in wrap(line, width, replace_whitespace=False,
  3374.                 initial_indent=initial_indent, subsequent_indent=subsequent_indent):
  3375.             yield line
  3376.         initial_indent = subsequent_indent
  3377.  
  3378. def ljust_columns(text, max_width, padchar=' '):
  3379.     """ Returns a string that is exactly <max_width> display columns wide,
  3380.    padded with <padchar> if necessary. Accounts for characters that are
  3381.    displayed two columns wide, i.e. kanji. """
  3382.  
  3383.     chars = []
  3384.     columns = 0
  3385.     max_width = max(0, max_width)
  3386.     for character in text:
  3387.         width = len_columns(character)
  3388.         if columns + width <= max_width:
  3389.             chars.append(character)
  3390.             columns += width
  3391.         else:
  3392.             break
  3393.  
  3394.     # Fill up any remaining space
  3395.     while columns < max_width:
  3396.         assert len(padchar) == 1
  3397.         chars.append(padchar)
  3398.         columns += 1
  3399.     return ''.join(chars)
  3400.  
  3401. def len_columns(text):
  3402.     """ Returns the amount of columns that <text> would occupy. """
  3403.     if isinstance(text, str):
  3404.         text = unicode(text, ENCODING)
  3405.     columns = 0
  3406.     for character in text:
  3407.         columns += 2 if unicodedata.east_asian_width(character) in ('W', 'F') else 1
  3408.     return columns
  3409.  
  3410.  
  3411. def num2str(num, format='%s'):
  3412.     if int(num) == -1:
  3413.         return '?'
  3414.     elif int(num) == -2:
  3415.         return 'oo'
  3416.     else:
  3417.         if num > 999:
  3418.             return (re.sub(r'(\d{3})', '\g<1>,', str(num)[::-1])[::-1]).lstrip(',')
  3419.         else:
  3420.             return format % num
  3421.  
  3422.  
  3423. def debug(data):
  3424.     if cmd_args.DEBUG:
  3425.         file = open("debug.log", 'a')
  3426.         if isinstance(data, str) or isinstance(data, unicode):
  3427.             file.write(data.encode('UTF-8', 'replace'))
  3428.         else:
  3429.             import pprint
  3430.             pp = pprint.PrettyPrinter(indent=4)
  3431.             file.write("\n====================\n" + pp.pformat(data) + "\n====================\n\n")
  3432.         file.close
  3433.  
  3434. def quit(msg='', exitcode=0):
  3435.     try:
  3436.         curses.endwin()
  3437.     except curses.error:
  3438.         pass
  3439.  
  3440.     # if this is a graceful exit and config file is present
  3441.     if not msg and not exitcode:
  3442.         save_config(cmd_args.configfile)
  3443.     else:
  3444.         print >> sys.stderr, msg,
  3445.     os._exit(exitcode)
  3446.  
  3447.  
  3448. def explode_connection_string(connection):
  3449.     host, port, path = \
  3450.         config.get('Connection', 'host'), \
  3451.         config.getint('Connection', 'port'),  \
  3452.         config.get('Connection', 'path')
  3453.     username, password = \
  3454.         config.get('Connection', 'username'), \
  3455.         config.get('Connection', 'password')
  3456.     try:
  3457.         if connection.count('@') == 1:
  3458.             auth, connection = connection.split('@')
  3459.             if auth.count(':') == 1:
  3460.                 username, password = auth.split(':')
  3461.         if connection.count(':') == 1:
  3462.             host, port = connection.split(':')
  3463.             if port.count('/') >= 1:
  3464.                 port, path = port.split('/', 1)
  3465.             port = int(port)
  3466.         else:
  3467.             host = connection
  3468.     except ValueError:
  3469.         quit("Wrong connection pattern: %s\n" % connection)
  3470.     return host, port, path, username, password
  3471.  
  3472. def create_url(host, port, path):
  3473.     url = '%s:%d/%s' % (host, port, path)
  3474.     url = url.replace('//', '/')   # double-/ doesn't work for some reason
  3475.     if config.getboolean('Connection', 'ssl'):
  3476.         return 'https://%s' % url
  3477.     else:
  3478.         return 'http://%s' % url
  3479.  
  3480. def read_netrc(file=os.environ['HOME'] + '/.netrc', hostname=None):
  3481.     try:
  3482.         login = password = ''
  3483.         try:
  3484.             login, account, password = netrc.netrc(file).authenticators(hostname)
  3485.         except TypeError:
  3486.             pass
  3487.         try:
  3488.             netrc.netrc(file).hosts[hostname]
  3489.         except KeyError:
  3490.             if hostname != 'localhost':
  3491.                 print "Unknown machine in %s: %s" % (file, hostname)
  3492.                 if login and password:
  3493.                     print "Using default login: %s" % login
  3494.                 else:
  3495.                     exit(CONFIGFILE_ERROR)
  3496.     except netrc.NetrcParseError, e:
  3497.         quit("Error in %s at line %s: %s\n" % (e.filename, e.lineno, e.msg))
  3498.     except IOError, msg:
  3499.         quit("Cannot read %s: %s\n" % (file, msg))
  3500.     return login, password
  3501.  
  3502.  
  3503. # create initial config file
  3504. def create_config(option, opt_str, value, parser):
  3505.     configfile = parser.values.configfile
  3506.     config.read(configfile)
  3507.     if parser.values.connection:
  3508.         host, port, path, username, password = explode_connection_string(parser.values.connection)
  3509.         config.set('Connection', 'host', host)
  3510.         config.set('Connection', 'port', str(port))
  3511.         config.set('Connection', 'path', path)
  3512.         config.set('Connection', 'username', username)
  3513.         config.set('Connection', 'password', password)
  3514.  
  3515.     # create directory if necessary
  3516.     dir = os.path.dirname(configfile)
  3517.     if dir != '' and not os.path.isdir(dir):
  3518.         try:
  3519.             os.makedirs(dir)
  3520.         except OSError, msg:
  3521.             print msg
  3522.             exit(CONFIGFILE_ERROR)
  3523.  
  3524.     # write file
  3525.     if not save_config(configfile, force=True):
  3526.         exit(CONFIGFILE_ERROR)
  3527.     print "Wrote config file: %s" % configfile
  3528.     exit(0)
  3529.  
  3530. def save_config(filepath, force=False):
  3531.     if force or os.path.isfile(filepath):
  3532.         try:
  3533.             config.write(open(filepath, 'w'))
  3534.             os.chmod(filepath, 0600)  # config may contain password
  3535.             return 1
  3536.         except IOError, msg:
  3537.             print >> sys.stderr, "Cannot write config file %s:\n%s" % (filepath, msg)
  3538.             return 0
  3539.     return -1
  3540.  
  3541. def parse_sort_str(sort_str):
  3542.     sort_orders = []
  3543.     for i in sort_str.split(','):
  3544.         x = i.split(':')
  3545.         if len(x) > 1:
  3546.             sort_orders.append( { 'name':x[1], 'reverse':True } )
  3547.         else:
  3548.             sort_orders.append( { 'name':x[0], 'reverse':False } )
  3549.     return sort_orders
  3550.  
  3551. def show_version(option, opt_str, value, parser):
  3552.     quit("transmission-remote-cli %s  (supports Transmission %s-%s)\n" % \
  3553.          (VERSION, TRNSM_VERSION_MIN, TRNSM_VERSION_MAX))
  3554.  
  3555.  
  3556. if __name__ == '__main__':
  3557.     # command line parameters
  3558.     default_config_path = os.environ['HOME'] + '/.config/transmission-remote-cli/settings.cfg'
  3559.     parser = OptionParser(usage="%prog [options] [-- transmission-remote options]",
  3560.                           description="%%prog %s" % VERSION)
  3561.     parser.add_option("-v", "--version", action="callback", callback=show_version,
  3562.                       help="Show version number and supported Transmission versions.")
  3563.     parser.add_option("-c", "--connect", action="store", dest="connection", default="",
  3564.                       help="Point to the server using pattern [username:password@]host[:port]/[path]")
  3565.     parser.add_option("-s", "--ssl", action="store_true", dest="ssl", default=False,
  3566.                       help="Connect to Transmission using SSL.")
  3567.     parser.add_option("-f", "--config", action="store", dest="configfile", default=default_config_path,
  3568.                       help="Path to configuration file.")
  3569.     parser.add_option("--create-config", action="callback", callback=create_config,
  3570.                       help="Create configuration file CONFIGFILE with default values.")
  3571.     parser.add_option("-n", "--netrc", action="store_true", dest="use_netrc", default=False,
  3572.                       help="Get authentication info from your ~/.netrc file.")
  3573.     parser.add_option("--debug", action="store_true", dest="DEBUG", default=False,
  3574.                       help="Everything passed to the debug() function will be added to the file debug.log.")
  3575.     (cmd_args, transmissionremote_args) = parser.parse_args()
  3576.  
  3577.  
  3578.     # read config from config file
  3579.     config.read(cmd_args.configfile)
  3580.  
  3581.     # command line connection data can override config file
  3582.     if cmd_args.connection:
  3583.         host, port, path, username, password = explode_connection_string(cmd_args.connection)
  3584.         config.set('Connection', 'host', host)
  3585.         config.set('Connection', 'port', str(port))
  3586.         config.set('Connection', 'path', path)
  3587.         config.set('Connection', 'username', username)
  3588.         config.set('Connection', 'password', password)
  3589.     if cmd_args.use_netrc:
  3590.         username, password = read_netrc(hostname=config.get('Connection','host'))
  3591.         config.set('Connection', 'username', username)
  3592.         config.set('Connection', 'password', password)
  3593.     if cmd_args.ssl:
  3594.         config.set('Connection', 'ssl', 'True')
  3595.  
  3596.  
  3597.  
  3598.     # forward arguments after '--' to transmission-remote
  3599.     if transmissionremote_args:
  3600.         cmd = ['transmission-remote', '%s:%s' %
  3601.                (config.get('Connection', 'host'), config.get('Connection', 'port'))]
  3602.         if find_executable(cmd[0]) is None:
  3603.             quit("Command not found: %s\n" % cmd[0], 128)
  3604.  
  3605.         # one argument and it doesn't start with '-' --> treat it like it's a torrent link/url
  3606.         if len(transmissionremote_args) == 1 and not transmissionremote_args[0].startswith('-'):
  3607.             cmd.extend(['-a', transmissionremote_args[0]])
  3608.         else:
  3609.             cmd.extend(transmissionremote_args)
  3610.  
  3611.         if config.get('Connection', 'username') and config.get('Connection', 'password'):
  3612.             cmd_print = cmd
  3613.             cmd_print.extend(['--auth', '%s:PASSWORD' % config.get('Connection', 'username')])
  3614.             print "EXECUTING:\n%s\nRESPONSE:" % ' '.join(cmd_print)
  3615.             cmd.extend(['--auth', '%s:%s' % (config.get('Connection', 'username'), config.get('Connection', 'password'))])
  3616.         else:
  3617.             print "EXECUTING:\n%s\nRESPONSE:" % ' '.join(cmd)
  3618.  
  3619.         try:
  3620.             retcode = call(cmd)
  3621.         except OSError, msg:
  3622.             quit("Could not execute the above command: %s\n" % msg.strerror, 128)
  3623.         quit('', retcode)
  3624.  
  3625.  
  3626.     norm = Normalizer()
  3627.     server = Transmission(config.get('Connection', 'host'),
  3628.                           config.getint('Connection', 'port'),
  3629.                           config.get('Connection', 'path'),
  3630.                           config.get('Connection', 'username'),
  3631.                           config.get('Connection', 'password'))
  3632.     Interface()
Add Comment
Please, Sign In to add comment