/btlaunchmanycurses.py
Python | 380 lines | 366 code | 11 blank | 3 comment | 24 complexity | 41979cfdab57b552973ca24a3bae644f MD5 | raw file
- #!/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 platform, argv, exit
- from time import time, localtime, strftime
- import sys, os
- from BitTornado import version, report_email
- from BitTornado.ConfigDir import ConfigDir
-
- try:
- import curses
- import curses.panel
- WCURSES = platform[:3] == "win" and curses.version.startswith("WCurses")
- if WCURSES:
- from curses import wrapper as curses_wrapper
- else:
- from curses.wrapper import wrapper as curses_wrapper
- 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). ' \
- 'You can also grab this instead http://adamv.com/dev/python/curses.'
- print ''
- print 'You may still use "btlaunchmany.py" for console interface.'
- print 'You may still use "lh-abc.py" for Graphical User Interface.'
- sys.exit(1)
-
- try:
- from signal import signal, SIGWINCH
- HAS_SIG = 1
- except:
- HAS_SIG = 0
-
- 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
- try:
- n = int(n)
- assert n < 5184000 # 60 days
- except:
- return 'connecting to peers'
- m, s = divmod(n, 60)
- h, m = divmod(m, 60)
- return 'ETA in %d:%02d:%02d' % (h, m, s)
-
- def fmtsize(n):
- n = long(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):
- if len(s) > size:
- s = s[:size-3] + '...'
- return s + (' '*(size-len(s)))
-
- def rjust(s, size):
- s = s[:size]
- return (' '*(size-len(s)))+s
-
- my_bg = curses.COLOR_BLACK
- def set_color(win, color):
- if curses.has_colors():
- n = color + 1
- curses.init_pair(n, color, my_bg)
- win.attroff(curses.A_COLOR)
- win.attron(curses.color_pair(n))
-
- def unset_color(win):
- if curses.has_colors():
- win.attrset(curses.color_pair(0))
-
- class CursesDisplayer:
- def __init__(self, scrwin):
- self.messages = []
- self.scroll_pos = 0
- self.scroll_time = 0
-
- self.scrwin = scrwin
- if HAS_SIG:
- 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)
-
- try:
- self.scrwin.border(ord('|'),ord('|'),ord('-'),ord('-'),ord(' '),ord(' '),ord(' '),ord(' '))
- except:
- pass
- if WCURSES:
- set_color(self.headerwin, curses.COLOR_GREEN)
- set_color(self.totalwin, curses.COLOR_GREEN)
- self.headerwin.addstr(0, 2, '#')
- self.headerwin.addstr(0, 4, 'Filename')
- self.headerwin.addstr(0, self.mainwinw - 24, 'Size')
- self.headerwin.addstr(0, self.mainwinw - 18, 'Download')
- self.headerwin.addstr(0, self.mainwinw - 6, 'Upload')
- self.totalwin.addstr(0, self.mainwinw - 27, 'Totals:')
- unset_color(self.totalwin)
- unset_color(self.headerwin)
- else:
- 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)
- if WCURSES:
- self.headerwin.refresh()
- self.totalwin.refresh()
- self._display_messages()
-
- 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.addstr(line, 0, s[:self.mainwinw], curses.A_BOLD)
- else:
- self.mainwin.addstr(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-33) + ' '
- 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 up %s dn - %s peers %s seeds %.3f copies' % (
- progress, status,
- fmtsize(upamt), fmtsize(dnamt),
- peers, seeds, dist )
- else:
- datastr = ' (%s) %s - %s up %s dn' % (
- progress, status,
- fmtsize(upamt), fmtsize(dnamt) )
- self._display_line(datastr)
- self._display_line(' '+ljust(msg,self.mainwinw-4))
- 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)
- elif WCURSES:
- self.mainwin.addstr( 1, int(self.mainwinw/2)-5,
- 'no torrents')
- 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()
- if WCURSES:
- self.totalwin.attrset(curses.A_BOLD)
- set_color(self.totalwin, curses.COLOR_GREEN)
- self.totalwin.addstr(0, self.mainwinw-27, 'Totals:')
- unset_color(self.totalwin)
- set_color(self.totalwin, curses.COLOR_WHITE)
- self.totalwin.addstr(0, self.mainwinw-20 + (10-len(totaldn)),
- totaldn)
- self.totalwin.addstr(0, self.mainwinw-10 + (10-len(totalup)),
- totalup)
- unset_color(self.totalwin)
- self.totalwin.attroff(curses.A_BOLD)
- else:
- 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)
-
- if WCURSES:
- self.mainwin.refresh()
- self.totalwin.refresh()
- curses.panel.update_panels()
- curses.doupdate()
- return inchar in (ord('q'),ord('Q'))
-
- 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:]:
- if WCURSES:
- self.statuswin.addstr(winpos, 0, s[:self.mainwinw])
- else:
- self.statuswin.addnstr(winpos, 0, s, self.mainwinw)
- winpos += 1
- if WCURSES:
- self.statuswin.refresh()
- curses.panel.update_panels()
- curses.doupdate()
-
- def exception(self, s):
- Exceptions.append(s)
- self.message('SYSTEM ERROR - EXCEPTION GENERATED')
-
-
-
- def LaunchManyWrapper(scrwin, configdir, config):
- LaunchMany(configdir, config, CursesDisplayer(scrwin))
-
-
- if __name__ == '__main__':
- if argv[1:] == ['--version']:
- print version
- exit(0)
- defaults.extend( [None,
- ( '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:
- configdir = ConfigDir('launchmanycurses')
- defaultsToIgnore = ['responsefile', 'url', 'priority']
- configdir.setDefaults(defaults,defaultsToIgnore)
- configdefaults = configdir.loadConfig()
- defaults.append(('save_options',0,
- "whether to save the current options as the new default configuration " +
- "(only for btlaunchmanycurses.py)"))
- if len(argv) < 2:
- print "Usage: btlaunchmanycurses.py <directory> <global options>\n"
- print "<directory> - directory to look for .torrent files (semi-recursive)"
- print get_usage(defaults, 80, configdefaults)
- exit(1)
- config, args = parseargs(argv[1:], defaults, 1, 1, configdefaults)
- if config['save_options']:
- configdir.saveConfig(config)
- configdir.deleteOldCacheData(config['expire_cache_data'])
- 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, configdir, config)
- if Exceptions:
- print '\nEXCEPTION:'
- print Exceptions[0]
- print 'please report this to '+report_email