0001#!/usr/bin/python
0002
0003# The contents of this file are subject to the BitTorrent Open Source License
0004# Version 1.0 (the License).  You may not copy or use this file, in either
0005# source code or executable form, except in compliance with the License.  You
0006# may obtain a copy of the License at http://www.bittorrent.com/license/.
0007#
0008# Software distributed under the License is distributed on an AS IS basis,
0009# WITHOUT WARRANTY OF ANY KIND, either express or implied.  See the License
0010# for the specific language governing rights and limitations under the
0011# License.
0012
0013# Written by John Hoffman
0014
0015# Updated (2005/10/29) by Philippe Normand to connect to a FmTorrent master
0016# instead of acting as a local Bt client
0017
0018from __future__ import division
0019
0020DOWNLOAD_SCROLL_RATE = 1
0021
0022import sys, os
0023from threading import Event
0024from time import time, localtime, strftime, sleep
0025from xmlrpclib import ServerProxy
0026
0027try:
0028    import curses
0029    import curses.panel
0030    from curses.wrapper import wrapper as curses_wrapper
0031    from signal import signal, SIGWINCH
0032except:
0033    print 'Textmode GUI initialization failed, cannot proceed.'
0034    print
0035    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).'
0039    print
0040    print 'You may still use "btdownloadheadless.py" to download.'
0041    sys.exit(1)
0042
0043exceptions = []
0044
0045def fmttime(n):
0046    if n <= 0:
0047        return None
0048    n = int(n)
0049    m, s = divmod(n, 60)
0050    h, m = divmod(m, 60)
0051    if h > 1000000:
0052        return 'connecting to peers'
0053    return 'ETA in %d:%02d:%02d' % (h, m, s)
0054
0055def fmtsize(n):
0056    n = long(n)
0057    unit = [' B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']
0058    i = 0
0059    if (n > 999):
0060        i = 1
0061        while i + 1 < len(unit) and (n >> 10) >= 999:
0062            i += 1
0063            n >>= 10
0064        n /= 1024
0065    if i > 0:
0066        size = '%.1f' % n + '%s' % unit[i]
0067    else:
0068        size = '%.0f' % n + '%s' % unit[i]
0069    return size
0070
0071def ljust(s, size):
0072    s = s[:size]
0073    return s + (' '*(size-len(s)))
0074
0075def rjust(s, size):
0076    s = s[:size]
0077    return (' '*(size-len(s)))+s
0078
0079
0080class CursesDisplayer(object):
0081
0082    def __init__(self, scrwin):
0083        self.messages = []
0084        self.scroll_pos = 0
0085        self.scroll_time = 0
0086
0087        self.scrwin = scrwin
0088        signal(SIGWINCH, self.winch_handler)
0089        self.changeflag = Event()
0090        self._remake_window()
0091
0092    def winch_handler(self, signum, stackframe):
0093        self.changeflag.set()
0094        curses.endwin()
0095        self.scrwin.refresh()
0096        self.scrwin = curses.newwin(0, 0, 0, 0)
0097        self._remake_window()
0098        self._display_messages()
0099
0100    def _remake_window(self):
0101        self.scrh, self.scrw = self.scrwin.getmaxyx()
0102        self.scrpan = curses.panel.new_panel(self.scrwin)
0103        self.mainwinh = (2*self.scrh)//3
0104        self.mainwinw = self.scrw - 4  # - 2 (bars) - 2 (spaces)
0105        self.mainwiny = 2         # + 1 (bar) + 1 (titles)
0106        self.mainwinx = 2         # + 1 (bar) + 1 (space)
0107        # + 1 to all windows so we can write at mainwinw
0108
0109        self.mainwin = curses.newwin(self.mainwinh, self.mainwinw+1,
0110                                     self.mainwiny, self.mainwinx)
0111        self.mainpan = curses.panel.new_panel(self.mainwin)
0112        self.mainwin.scrollok(0)
0113        self.mainwin.nodelay(1)
0114
0115        self.headerwin = curses.newwin(1, self.mainwinw+1,
0116                                       1, self.mainwinx)
0117        self.headerpan = curses.panel.new_panel(self.headerwin)
0118        self.headerwin.scrollok(0)
0119
0120        self.totalwin = curses.newwin(1, self.mainwinw+1,
0121                                      self.mainwinh+1, self.mainwinx)
0122        self.totalpan = curses.panel.new_panel(self.totalwin)
0123        self.totalwin.scrollok(0)
0124
0125        self.statuswinh = self.scrh-4-self.mainwinh
0126        self.statuswin = curses.newwin(self.statuswinh, self.mainwinw+1,
0127                                       self.mainwinh+3, self.mainwinx)
0128        self.statuspan = curses.panel.new_panel(self.statuswin)
0129        self.statuswin.scrollok(0)
0130
0131        try:
0132            self.scrwin.border(ord('|'),ord('|'),ord('-'),ord('-'),ord(' '),ord(' '),ord(' '),ord(' '))
0133        except:
0134            pass
0135        self.headerwin.addnstr(0, 2, '#', self.mainwinw - 25, curses.A_BOLD)
0136        self.headerwin.addnstr(0, 4, 'Filename', self.mainwinw - 25, curses.A_BOLD)
0137        self.headerwin.addnstr(0, self.mainwinw - 24, 'Size', 4, curses.A_BOLD)
0138        self.headerwin.addnstr(0, self.mainwinw - 18, 'Download', 8, curses.A_BOLD)
0139        self.headerwin.addnstr(0, self.mainwinw -  6, 'Upload', 6, curses.A_BOLD)
0140        self.totalwin.addnstr(0, self.mainwinw - 27, 'Totals:', 7, curses.A_BOLD)
0141
0142        self._display_messages()
0143
0144        curses.panel.update_panels()
0145        curses.doupdate()
0146        self.changeflag.clear()
0147
0148
0149    def _display_line(self, s, bold = False):
0150        if self.disp_end:
0151            return True
0152        line = self.disp_line
0153        self.disp_line += 1
0154        if line < 0:
0155            return False
0156        if bold:
0157            self.mainwin.addnstr(line, 0, s, self.mainwinw, curses.A_BOLD)
0158        else:
0159            self.mainwin.addnstr(line, 0, s, self.mainwinw)
0160        if self.disp_line >= self.mainwinh:
0161            self.disp_end = True
0162        return self.disp_end
0163
0164    def _display_data(self, data):
0165        if 3*len(data) <= self.mainwinh:
0166            self.scroll_pos = 0
0167            self.scrolling = False
0168        elif self.scroll_time + DOWNLOAD_SCROLL_RATE < time():
0169            self.scroll_time = time()
0170            self.scroll_pos += 1
0171            self.scrolling = True
0172            if self.scroll_pos >= 3*len(data)+2:
0173                self.scroll_pos = 0
0174
0175        i = self.scroll_pos//3
0176        self.disp_line = (3*i)-self.scroll_pos
0177        self.disp_end = False
0178
0179        while not self.disp_end:
0180            ii = i % len(data)
0181            if i and not ii:
0182                if not self.scrolling:
0183                    break
0184                self._display_line('')
0185                if self._display_line(''):
0186                    break
0187            ( name, status, progress, peers, seeds, seedsmsg, dist,
0188              uprate, dnrate, upamt, dnamt, size, t, msg ) = data[ii]
0189            t = fmttime(t)
0190            if t:
0191                status = t
0192            name = ljust(name,self.mainwinw-32)
0193            size = rjust(fmtsize(size),8)
0194            uprate = rjust('%s/s' % fmtsize(uprate),10)
0195            dnrate = rjust('%s/s' % fmtsize(dnrate),10)
0196            line = "%3d %s%s%s%s" % (ii+1, name, size, dnrate, uprate)
0197            self._display_line(line, True)
0198            if peers + seeds:
0199                datastr = '    (%s) %s - %s peers %s seeds %s dist copies - %s up %s dn' % (
0200                                progress, status, peers, seeds, dist,
0201                                fmtsize(upamt), fmtsize(dnamt) )
0202            else:
0203                datastr = '    '+status+' ('+progress+')'
0204            self._display_line(datastr)
0205            self._display_line('    '+ljust(msg,self.mainwinw-4))
0206            i += 1
0207
0208    def display(self, name, data):
0209        if self.changeflag.isSet():
0210            return
0211
0212        inchar = self.mainwin.getch()
0213        if inchar == 12: # ^L
0214            self._remake_window()
0215
0216        self.mainwin.erase()
0217        if data:
0218            self._display_data(data)
0219        else:
0220            self.mainwin.addnstr( 1, self.mainwinw//2-5,
0221                                  'no torrents', 12, curses.A_BOLD )
0222        totalup = 0
0223        totaldn = 0
0224        for ( name, status, progress, peers, seeds, seedsmsg, dist,
0225              uprate, dnrate, upamt, dnamt, size, t, msg ) in data:
0226            totalup += uprate
0227            totaldn += dnrate
0228
0229        totalup = '%s/s' % fmtsize(totalup)
0230        totaldn = '%s/s' % fmtsize(totaldn)
0231
0232        self.totalwin.erase()
0233        self.totalwin.addnstr(0, self.mainwinw-27, 'Totals:', 7, curses.A_BOLD)
0234        self.totalwin.addnstr(0, self.mainwinw-20 + (10-len(totaldn)),
0235                              totaldn, 10, curses.A_BOLD)
0236        self.totalwin.addnstr(0, self.mainwinw-10 + (10-len(totalup)),
0237                              totalup, 10, curses.A_BOLD)
0238
0239        curses.panel.update_panels()
0240        curses.doupdate()
0241
0242        return inchar in (ord('q'),ord('Q'))
0243
0244    def message(self, s):
0245        self.messages.append(strftime('%x %X - ',localtime(time()))+s)
0246        self._display_messages()
0247
0248    def _display_messages(self):
0249        self.statuswin.erase()
0250        winpos = 0
0251        for s in self.messages[-self.statuswinh:]:
0252            self.statuswin.addnstr(winpos, 0, s, self.mainwinw)
0253            winpos += 1
0254        curses.panel.update_panels()
0255        curses.doupdate()
0256
0257    def exception(self, s):
0258        exceptions.append(s)
0259        self.message('SYSTEM ERROR - EXCEPTION GENERATED')
0260
0261
0262def FmTorrentWrapper(scrwin, url):
0263    server = ServerProxy(url)
0264    disp = CursesDisplayer(scrwin)
0265    while 1:
0266        status = server.getStatus()
0267        data = [ (name, d['status'], d['progress'], d['peers'], d['seeds'],
0268                  d['seedsmsg'], d['dist'], d['uprate'], d['dnrate'], d['upamt'],
0269                  d['dnamt'], d['size'], d['t'], d['msg'])
0270                 for name, d in status.iteritems() ]
0271        disp.display("", data)
0272        sleep(1.0)
0273
0274if __name__ == '__main__':
0275    if len(sys.argv) < 2:
0276        print "usage: %s master_url" % sys.argv[0]
0277    else:
0278        url = sys.argv[-1]
0279        try:
0280            curses_wrapper(FmTorrentWrapper, url)
0281        except KeyboardInterrupt:
0282            pass