summaryrefslogtreecommitdiffstats
path: root/bittorrent-curses.py
diff options
context:
space:
mode:
Diffstat (limited to 'bittorrent-curses.py')
-rwxr-xr-xbittorrent-curses.py450
1 files changed, 450 insertions, 0 deletions
diff --git a/bittorrent-curses.py b/bittorrent-curses.py
new file mode 100755
index 0000000..a4dc9a3
--- /dev/null
+++ b/bittorrent-curses.py
@@ -0,0 +1,450 @@
+#!/usr/bin/env python
+
+# The contents of this file are subject to the BitTorrent Open Source License
+# Version 1.1 (the License). You may not copy or use this file, in either
+# source code or executable form, except in compliance with the License. You
+# may obtain a copy of the License at http://www.bittorrent.com/license/.
+#
+# Software distributed under the License is distributed on an AS IS basis,
+# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+# for the specific language governing rights and limitations under the
+# License.
+
+# Original version written by Henry 'Pi' James, modified by (at least)
+# John Hoffman and Uoti Urpala
+
+from __future__ import division
+
+from BitTorrent.platform import install_translation
+install_translation()
+
+SPEW_SCROLL_RATE = 1
+
+import sys
+import os
+import threading
+from time import time, strftime
+
+from BitTorrent.download import Feedback, Multitorrent
+from BitTorrent.defaultargs import get_defaults
+from BitTorrent.parseargs import printHelp
+from BitTorrent.zurllib import urlopen
+from BitTorrent.bencode import bdecode
+from BitTorrent.ConvertedMetainfo import ConvertedMetainfo
+from BitTorrent.prefs import Preferences
+from BitTorrent.obsoletepythonsupport import import_curses
+from BitTorrent import configfile
+from BitTorrent import BTFailure
+from BitTorrent import version
+from BitTorrent import GetTorrent
+
+
+try:
+ curses = import_curses()
+ import curses.panel
+ from curses.wrapper import wrapper as curses_wrapper
+ from signal import signal, SIGWINCH
+except:
+ print _("Textmode GUI initialization failed, cannot proceed.")
+ print
+ print _("This download interface requires the standard Python module "
+ "\"curses\", which is unfortunately not available for the native "
+ "Windows port of Python. It is however available for the Cygwin "
+ "port of Python, running on all Win32 systems (www.cygwin.com).")
+ print
+ print _('You may still use "bittorrent-console" to download.')
+ sys.exit(1)
+
+def fmttime(n):
+ if n == 0:
+ return _("download complete!")
+ try:
+ n = int(n)
+ assert n >= 0 and n < 5184000 # 60 days
+ except:
+ return _("<unknown>")
+ m, s = divmod(n, 60)
+ h, m = divmod(m, 60)
+ return _("finishing in %d:%02d:%02d") % (h, m, s)
+
+def fmtsize(n):
+ s = str(n)
+ size = s[-3:]
+ while len(s) > 3:
+ s = s[:-3]
+ size = '%s,%s' % (s[-3:], size)
+ if n > 999:
+ unit = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']
+ i = 1
+ while i + 1 < len(unit) and (n >> 10) >= 999:
+ i += 1
+ n >>= 10
+ n /= (1 << 10)
+ size = '%s (%.0f %s)' % (size, n, unit[i])
+ return size
+
+
+class CursesDisplayer(object):
+
+ def __init__(self, scrwin, errlist, doneflag, reread_config, ulrate):
+ self.scrwin = scrwin
+ self.errlist = errlist
+ self.doneflag = doneflag
+
+ signal(SIGWINCH, self.winch_handler)
+ self.changeflag = threading.Event()
+
+ self.done = False
+ self.reread_config = reread_config
+ self.ulrate = ulrate
+ self.activity = ''
+ self.status = ''
+ self.progress = ''
+ self.downRate = '---'
+ self.upRate = '---'
+ self.shareRating = ''
+ self.seedStatus = ''
+ self.peerStatus = ''
+ self.errors = []
+ self.file = ''
+ self.downloadTo = ''
+ self.fileSize = ''
+ self.numpieces = 0
+ self.spew_scroll_time = 0
+ self.spew_scroll_pos = 0
+
+ self._remake_window()
+ curses.use_default_colors()
+
+ def set_torrent_values(self, name, path, size, numpieces):
+ self.file = name
+ self.downloadTo = path
+ self.fileSize = fmtsize(size)
+ self.numpieces = numpieces
+ self._remake_window()
+
+ def winch_handler(self, signum, stackframe):
+ self.changeflag.set()
+ curses.endwin()
+ self.scrwin.refresh()
+ self.scrwin = curses.newwin(0, 0, 0, 0)
+ self._remake_window()
+
+ def _remake_window(self):
+ self.scrh, self.scrw = self.scrwin.getmaxyx()
+ self.scrpan = curses.panel.new_panel(self.scrwin)
+ self.labelh, self.labelw, self.labely, self.labelx = 11, 9, 1, 2
+ self.labelwin = curses.newwin(self.labelh, self.labelw,
+ self.labely, self.labelx)
+ self.labelpan = curses.panel.new_panel(self.labelwin)
+ self.fieldh, self.fieldw, self.fieldy, self.fieldx = (
+ self.labelh, self.scrw-2 - self.labelw-3,
+ 1, self.labelw+3)
+ self.fieldwin = curses.newwin(self.fieldh, self.fieldw,
+ self.fieldy, self.fieldx)
+ self.fieldwin.nodelay(1)
+ self.fieldpan = curses.panel.new_panel(self.fieldwin)
+ self.spewh, self.speww, self.spewy, self.spewx = (
+ self.scrh - self.labelh - 2, self.scrw - 3, 1 + self.labelh, 2)
+ self.spewwin = curses.newwin(self.spewh, self.speww,
+ self.spewy, self.spewx)
+ self.spewpan = curses.panel.new_panel(self.spewwin)
+ try:
+ self.scrwin.border(ord('|'),ord('|'),ord('-'),ord('-'),ord(' '),ord(' '),ord(' '),ord(' '))
+ except:
+ pass
+ self.labelwin.addstr(0, 0, _("file:"))
+ self.labelwin.addstr(1, 0, _("size:"))
+ self.labelwin.addstr(2, 0, _("dest:"))
+ self.labelwin.addstr(3, 0, _("progress:"))
+ self.labelwin.addstr(4, 0, _("status:"))
+ self.labelwin.addstr(5, 0, _("dl speed:"))
+ self.labelwin.addstr(6, 0, _("ul speed:"))
+ self.labelwin.addstr(7, 0, _("sharing:"))
+ self.labelwin.addstr(8, 0, _("seeds:"))
+ self.labelwin.addstr(9, 0, _("peers:"))
+ curses.panel.update_panels()
+ curses.doupdate()
+ self.changeflag.clear()
+
+
+ def finished(self):
+ self.done = True
+ self.downRate = '---'
+ self.display({'activity':_("download succeeded"), 'fractionDone':1})
+
+ def error(self, errormsg):
+ newerrmsg = strftime('[%H:%M:%S] ') + errormsg
+ self.errors.append(newerrmsg.split('\n')[0])
+ self.errlist.append(newerrmsg)
+ self.display({})
+
+ def display(self, statistics):
+ fractionDone = statistics.get('fractionDone')
+ activity = statistics.get('activity')
+ timeEst = statistics.get('timeEst')
+ downRate = statistics.get('downRate')
+ upRate = statistics.get('upRate')
+ spew = statistics.get('spew')
+
+ inchar = self.fieldwin.getch()
+ if inchar == 12: # ^L
+ self._remake_window()
+ elif inchar in (ord('q'),ord('Q')):
+ self.doneflag.set()
+ elif inchar in (ord('r'),ord('R')):
+ self.reread_config()
+ elif inchar in (ord('u'),ord('U')):
+ curses.echo()
+ self.fieldwin.nodelay(0)
+ s = self.fieldwin.getstr(6,10)
+ curses.noecho()
+ self.fieldwin.nodelay(1)
+ r = None
+ try:
+ r = int(s)
+ except ValueError:
+ pass
+ if r is not None:
+ self.ulrate(r)
+
+ if timeEst is not None:
+ self.activity = fmttime(timeEst)
+ elif activity is not None:
+ self.activity = activity
+ if self.changeflag.isSet():
+ return
+
+ if fractionDone is not None:
+ blocknum = int(self.fieldw * fractionDone)
+ self.progress = blocknum * '#' + (self.fieldw - blocknum) * '_'
+ self.status = '%s (%.1f%%)' % (self.activity, fractionDone * 100)
+
+ if downRate is not None:
+ self.downRate = '%.1f KB/s' % (downRate / (1 << 10))
+ if upRate is not None:
+ self.upRate = '%.1f KB/s' % (upRate / (1 << 10))
+ downTotal = statistics.get('downTotal')
+ if downTotal is not None:
+ upTotal = statistics['upTotal']
+ if downTotal <= upTotal / 100:
+ self.shareRating = _("oo (%.1f MB up / %.1f MB down)") % (
+ upTotal / (1<<20), downTotal / (1<<20))
+ else:
+ self.shareRating = _("%.3f (%.1f MB up / %.1f MB down)") % (
+ upTotal / downTotal, upTotal / (1<<20), downTotal / (1<<20))
+ numCopies = statistics['numCopies']
+ nextCopies = ', '.join(["%d:%.1f%%" % (a,int(b*1000)/10) for a,b in
+ zip(xrange(numCopies+1, 1000), statistics['numCopyList'])])
+ if not self.done:
+ self.seedStatus = _("%d seen now, plus %d distributed copies"
+ "(%s)") % (statistics['numSeeds' ],
+ statistics['numCopies'],
+ nextCopies)
+ else:
+ self.seedStatus = _("%d distributed copies (next: %s)") % (
+ statistics['numCopies'], nextCopies)
+ self.peerStatus = _("%d seen now") % statistics['numPeers']
+
+ self.fieldwin.erase()
+ self.fieldwin.addnstr(0, 0, self.file, self.fieldw, curses.A_BOLD)
+ self.fieldwin.addnstr(1, 0, self.fileSize, self.fieldw)
+ self.fieldwin.addnstr(2, 0, self.downloadTo, self.fieldw)
+ if self.progress:
+ self.fieldwin.addnstr(3, 0, self.progress, self.fieldw, curses.A_BOLD)
+ self.fieldwin.addnstr(4, 0, self.status, self.fieldw)
+ self.fieldwin.addnstr(5, 0, self.downRate, self.fieldw)
+ self.fieldwin.addnstr(6, 0, self.upRate, self.fieldw)
+ self.fieldwin.addnstr(7, 0, self.shareRating, self.fieldw)
+ self.fieldwin.addnstr(8, 0, self.seedStatus, self.fieldw)
+ self.fieldwin.addnstr(9, 0, self.peerStatus, self.fieldw)
+
+ self.spewwin.erase()
+
+ if not spew:
+ errsize = self.spewh
+ if self.errors:
+ self.spewwin.addnstr(0, 0, _("error(s):"), self.speww, curses.A_BOLD)
+ errsize = len(self.errors)
+ displaysize = min(errsize, self.spewh)
+ displaytop = errsize - displaysize
+ for i in range(displaysize):
+ self.spewwin.addnstr(i, self.labelw, self.errors[displaytop + i],
+ self.speww-self.labelw-1, curses.A_BOLD)
+ else:
+ if self.errors:
+ self.spewwin.addnstr(0, 0, _("error:"), self.speww, curses.A_BOLD)
+ self.spewwin.addnstr(0, self.labelw, self.errors[-1],
+ self.speww-self.labelw-1, curses.A_BOLD)
+ self.spewwin.addnstr(2, 0, _(" # IP Upload Download Completed Speed"), self.speww, curses.A_BOLD)
+
+
+ if self.spew_scroll_time + SPEW_SCROLL_RATE < time():
+ self.spew_scroll_time = time()
+ if len(spew) > self.spewh-5 or self.spew_scroll_pos > 0:
+ self.spew_scroll_pos += 1
+ if self.spew_scroll_pos > len(spew):
+ self.spew_scroll_pos = 0
+
+ for i in range(len(spew)):
+ spew[i]['lineno'] = i+1
+ spew.append({'lineno': None})
+ spew = spew[self.spew_scroll_pos:] + spew[:self.spew_scroll_pos]
+
+ for i in range(min(self.spewh - 5, len(spew))):
+ if not spew[i]['lineno']:
+ continue
+ self.spewwin.addnstr(i+3, 0, '%3d' % spew[i]['lineno'], 3)
+ self.spewwin.addnstr(i+3, 4, spew[i]['ip'], 15)
+ ul = spew[i]['upload']
+ if ul[1] > 100:
+ self.spewwin.addnstr(i+3, 20, '%6.0f KB/s' % (
+ ul[1] / 1000), 11)
+ self.spewwin.addnstr(i+3, 32, '-----', 5)
+ if ul[2]:
+ self.spewwin.addnstr(i+3, 33, 'I', 1)
+ if ul[3]:
+ self.spewwin.addnstr(i+3, 35, 'C', 1)
+ dl = spew[i]['download']
+ if dl[1] > 100:
+ self.spewwin.addnstr(i+3, 38, '%6.0f KB/s' % (
+ dl[1] / 1000), 11)
+ self.spewwin.addnstr(i+3, 50, '-------', 7)
+ if dl[2]:
+ self.spewwin.addnstr(i+3, 51, 'I', 1)
+ if dl[3]:
+ self.spewwin.addnstr(i+3, 53, 'C', 1)
+ if dl[4]:
+ self.spewwin.addnstr(i+3, 55, 'S', 1)
+ self.spewwin.addnstr(i+3, 58, '%5.1f%%' % (int(spew[i]['completed']*1000)/10), 6)
+ if spew[i]['speed'] is not None:
+ self.spewwin.addnstr(i+3, 64, '%5.0f KB/s' % (spew[i]['speed']/1000), 10)
+
+ self.spewwin.addnstr(self.spewh-1, 0,
+ _("downloading %d pieces, have %d fragments, "
+ "%d of %d pieces completed") %
+ (statistics['storage_active'], statistics['storage_dirty'],
+ statistics['storage_numcomplete'], self.numpieces),
+ self.speww-1)
+
+ curses.panel.update_panels()
+ curses.doupdate()
+
+
+class DL(Feedback):
+
+ def __init__(self, metainfo, config, errlist):
+ self.doneflag = threading.Event()
+ self.metainfo = metainfo
+ self.config = Preferences().initWithDict(config)
+ self.errlist = errlist
+
+ def run(self, scrwin):
+ def reread():
+ self.multitorrent.rawserver.external_add_task(self.reread_config,0)
+ def ulrate(value):
+ self.multitorrent.set_option('max_upload_rate', value)
+ self.torrent.set_option('max_upload_rate', value)
+
+ self.d = CursesDisplayer(scrwin, self.errlist, self.doneflag, reread, ulrate)
+ try:
+ self.multitorrent = Multitorrent(self.config, self.doneflag,
+ self.global_error)
+ # raises BTFailure if bad
+ metainfo = ConvertedMetainfo(bdecode(self.metainfo))
+ torrent_name = metainfo.name_fs
+ if config['save_as']:
+ if config['save_in']:
+ raise BTFailure(_("You cannot specify both --save_as and "
+ "--save_in"))
+ saveas = config['save_as']
+ elif config['save_in']:
+ saveas = os.path.join(config['save_in'], torrent_name)
+ else:
+ saveas = torrent_name
+
+ self.d.set_torrent_values(metainfo.name, os.path.abspath(saveas),
+ metainfo.total_bytes, len(metainfo.hashes))
+ self.torrent = self.multitorrent.start_torrent(metainfo,
+ Preferences(self.config), self, saveas)
+ except BTFailure, e:
+ errlist.append(str(e))
+ return
+ self.get_status()
+ self.multitorrent.rawserver.install_sigint_handler()
+ self.multitorrent.rawserver.listen_forever()
+ self.d.display({'activity':_("shutting down"), 'fractionDone':0})
+ self.torrent.shutdown()
+
+ def reread_config(self):
+ try:
+ newvalues = configfile.get_config(self.config, 'bittorrent-curses')
+ except Exception, e:
+ self.d.error(_("Error reading config: ") + str(e))
+ return
+ self.config.update(newvalues)
+ # The set_option call can potentially trigger something that kills
+ # the torrent (when writing this the only possibility is a change in
+ # max_files_open causing an IOError while closing files), and so
+ # the self.failed() callback can run during this loop.
+ for option, value in newvalues.iteritems():
+ self.multitorrent.set_option(option, value)
+ for option, value in newvalues.iteritems():
+ self.torrent.set_option(option, value)
+
+ def get_status(self):
+ self.multitorrent.rawserver.add_task(self.get_status,
+ self.config['display_interval'])
+ status = self.torrent.get_status(self.config['spew'])
+ self.d.display(status)
+
+ def global_error(self, level, text):
+ self.d.error(text)
+
+ def error(self, torrent, level, text):
+ self.d.error(text)
+
+ def failed(self, torrent, is_external):
+ self.doneflag.set()
+
+ def finished(self, torrent):
+ self.d.finished()
+
+
+if __name__ == '__main__':
+ uiname = 'bittorrent-curses'
+ defaults = get_defaults(uiname)
+
+ metainfo = None
+ if len(sys.argv) <= 1:
+ printHelp(uiname, defaults)
+ sys.exit(1)
+ try:
+ config, args = configfile.parse_configuration_and_args(defaults,
+ uiname, sys.argv[1:], 0, 1)
+
+ torrentfile = None
+ if len(args):
+ torrentfile = args[0]
+ for opt in ('responsefile', 'url'):
+ if config[opt]:
+ print '"--%s"' % opt, _("deprecated, do not use")
+ torrentfile = config[opt]
+ if torrentfile is not None:
+ metainfo, errors = GetTorrent.get(torrentfile)
+ if errors:
+ raise BTFailure(_("Error reading .torrent file: ") + '\n'.join(errors))
+ else:
+ raise BTFailure(_("you must specify a .torrent file"))
+ except BTFailure, e:
+ print str(e)
+ sys.exit(1)
+
+ errlist = []
+ dl = DL(metainfo, config, errlist)
+ curses_wrapper(dl.run)
+
+ if errlist:
+ print _("These errors occurred during execution:")
+ for error in errlist:
+ print error