#!/usr/bin/env python # Written by John Hoffman # see LICENSE.txt for license information DOWNLOAD_SCROLL_RATE = 1 from BitTornado import PSYCO if PSYCO.psyco: try: import psyco assert psyco.__version__ >= 0x010100f0 psyco.full() except: pass from BitTornado.launchmanycore import LaunchMany from BitTornado.download_bt1 import defaults, get_usage from BitTornado.parseargs import parseargs from threading import Event from sys import argv, exit from time import time, localtime, strftime import sys, os from BitTornado import version, report_email try: 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 "btdownloadheadless.py" to download.' sys.exit(1) assert sys.version >= '2', "Install Python 2.0 or greater" try: True except: True = 1 False = 0 Exceptions = [] def fmttime(n): if n <= 0: return None n = int(n) m, s = divmod(n, 60) h, m = divmod(m, 60) if h > 1000000: return 'connecting to peers' return 'ETA in %d:%02d:%02d' % (h, m, s) def fmtsize(n): n = int(n) unit = [' B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] i = 0 if (n > 999): i = 1 while i + 1 < len(unit) and (n >> 10) >= 999: i += 1 n >>= 10 n = float(n) / (1 << 10) if i > 0: size = '%.1f' % n + '%s' % unit[i] else: size = '%.0f' % n + '%s' % unit[i] return size def ljust(s, size): s = s[:size] return s + (' '*(size-len(s))) def rjust(s, size): s = s[:size] return (' '*(size-len(s)))+s class CursesDisplayer: def __init__(self, scrwin): self.messages = [] self.scroll_pos = 0 self.scroll_time = 0 self.scrwin = scrwin signal(SIGWINCH, self.winch_handler) self.changeflag = Event() 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() self._display_messages() def _remake_window(self): self.scrh, self.scrw = self.scrwin.getmaxyx() self.scrpan = curses.panel.new_panel(self.scrwin) self.mainwinh = int(2*(self.scrh)/3) self.mainwinw = self.scrw - 4 # - 2 (bars) - 2 (spaces) self.mainwiny = 2 # + 1 (bar) + 1 (titles) self.mainwinx = 2 # + 1 (bar) + 1 (space) # + 1 to all windows so we can write at mainwinw self.mainwin = curses.newwin(self.mainwinh, self.mainwinw+1, self.mainwiny, self.mainwinx) self.mainpan = curses.panel.new_panel(self.mainwin) self.mainwin.scrollok(0) self.mainwin.nodelay(1) self.headerwin = curses.newwin(1, self.mainwinw+1, 1, self.mainwinx) self.headerpan = curses.panel.new_panel(self.headerwin) self.headerwin.scrollok(0) self.totalwin = curses.newwin(1, self.mainwinw+1, self.mainwinh+1, self.mainwinx) self.totalpan = curses.panel.new_panel(self.totalwin) self.totalwin.scrollok(0) self.statuswinh = self.scrh-4-self.mainwinh self.statuswin = curses.newwin(self.statuswinh, self.mainwinw+1, self.mainwinh+3, self.mainwinx) self.statuspan = curses.panel.new_panel(self.statuswin) self.statuswin.scrollok(0) self.scrwin.border(ord('|'),ord('|'),ord('-'),ord('-'),ord(' '),ord(' '),ord(' '),ord(' ')) self.headerwin.addnstr(0, 2, '#', self.mainwinw - 25, curses.A_BOLD) self.headerwin.addnstr(0, 4, 'Filename', self.mainwinw - 25, curses.A_BOLD) self.headerwin.addnstr(0, self.mainwinw - 24, 'Size', 4, curses.A_BOLD) self.headerwin.addnstr(0, self.mainwinw - 18, 'Download', 8, curses.A_BOLD) self.headerwin.addnstr(0, self.mainwinw - 6, 'Upload', 6, curses.A_BOLD) self.totalwin.addnstr(0, self.mainwinw - 27, 'Totals:', 7, curses.A_BOLD) curses.panel.update_panels() curses.doupdate() self.changeflag.clear() def _display_line(self, s, bold = False): if self.disp_end: return True line = self.disp_line self.disp_line += 1 if line < 0: return False if bold: self.mainwin.addnstr(line, 0, s, self.mainwinw, curses.A_BOLD) else: self.mainwin.addnstr(line, 0, s, self.mainwinw) if self.disp_line >= self.mainwinh: self.disp_end = True return self.disp_end def _display_data(self, data): if 3*len(data) <= self.mainwinh: self.scroll_pos = 0 self.scrolling = False elif self.scroll_time + DOWNLOAD_SCROLL_RATE < time(): self.scroll_time = time() self.scroll_pos += 1 self.scrolling = True if self.scroll_pos >= 3*len(data)+2: self.scroll_pos = 0 i = int(self.scroll_pos/3) self.disp_line = (3*i)-self.scroll_pos self.disp_end = False while not self.disp_end: ii = i % len(data) if i and not ii: if not self.scrolling: break self._display_line('') if self._display_line(''): break ( name, status, progress, peers, seeds, seedsmsg, dist, uprate, dnrate, upamt, dnamt, size, t, msg ) = data[ii] t = fmttime(t) if t: status = t name = ljust(name,self.mainwinw-32) size = rjust(fmtsize(size),8) uprate = rjust('%s/s' % fmtsize(uprate),10) dnrate = rjust('%s/s' % fmtsize(dnrate),10) line = "%3d %s%s%s%s" % (ii+1, name, size, dnrate, uprate) self._display_line(line, True) if peers + seeds: datastr = ' (%s) %s - %s peers %s seeds %.3f dist copies - %s up %s dn' % ( progress, status, peers, seeds, dist, fmtsize(upamt), fmtsize(dnamt) ) else: datastr = ' '+status+' ('+progress+')' self._display_line(datastr) self._display_line(' '+msg) i += 1 def display(self, data): if self.changeflag.isSet(): return inchar = self.mainwin.getch() if inchar == 12: # ^L self._remake_window() self.mainwin.erase() if data: self._display_data(data) else: self.mainwin.addnstr( 1, int(self.mainwinw/2)-5, 'no torrents', 12, curses.A_BOLD ) totalup = 0 totaldn = 0 for ( name, status, progress, peers, seeds, seedsmsg, dist, uprate, dnrate, upamt, dnamt, size, t, msg ) in data: totalup += uprate totaldn += dnrate totalup = '%s/s' % fmtsize(totalup) totaldn = '%s/s' % fmtsize(totaldn) self.totalwin.erase() self.totalwin.addnstr(0, self.mainwinw-27, 'Totals:', 7, curses.A_BOLD) self.totalwin.addnstr(0, self.mainwinw-20 + (10-len(totaldn)), totaldn, 10, curses.A_BOLD) self.totalwin.addnstr(0, self.mainwinw-10 + (10-len(totalup)), totalup, 10, curses.A_BOLD) curses.panel.update_panels() curses.doupdate() return chr(inchar) in 'qQ' def message(self, s): self.messages.append(strftime('%x %X - ',localtime(time()))+s) self._display_messages() def _display_messages(self): self.statuswin.erase() winpos = 0 for s in self.messages[-self.statuswinh:]: self.statuswin.addnstr(winpos, 0, s, self.mainwinw) winpos += 1 curses.panel.update_panels() curses.doupdate() def exception(self, s): Exceptions.append(s) self.message('SYSTEM ERROR - EXCEPTION GENERATED') def LaunchManyWrapper(scrwin, config): LaunchMany(config, CursesDisplayer(scrwin)) if __name__ == '__main__': if argv[1:] == ['--version']: print version exit(0) defaults.extend( [ ( 'parse_dir_interval', 60, "how often to rescan the torrent directory, in seconds" ), ( 'saveas_style', 2, "How to name torrent downloads (1 = rename to torrent name, " + "2 = save under name in torrent, 3 = save in directory under torrent name)" ), ( 'display_path', 0, "whether to display the full path or the torrent contents for each torrent" ), ] ) try: if len(argv) <= 1: print "Usage: btlaunchmany.py \n" print " - directory to look for .torrent files (semi-recursive)" print get_usage(defaults) exit(1) config, args = parseargs(argv[1:], defaults, 1, 1) if not os.path.isdir(args[0]): raise ValueError("Warning: "+args[0]+" is not a directory") config['torrent_dir'] = args[0] except ValueError, e: print 'error: ' + str(e) + '\nrun with no args for parameter explanations' exit(1) curses_wrapper(LaunchManyWrapper, config) if Exceptions: print '\nEXCEPTION:' print Exceptions[0] print 'please report this to '+report_email