home *** CD-ROM | disk | FTP | other *** search
/ Personal Computer World 2007 September / PCWSEP07.iso / Software / Linux / Linux Mint 3.0 Light / LinuxMint-3.0-Light.iso / casper / filesystem.squashfs / usr / bin / btlaunchmanycurses.bittorrent < prev    next >
Encoding:
Text File  |  2007-02-19  |  12.9 KB  |  343 lines

  1. #! /usr/bin/python
  2.  
  3. # Written by Michael Janssen (jamuraa at base0 dot net)
  4. # originally heavily borrowed code from btlaunchmany.py by Bram Cohen
  5. # and btdownloadcurses.py written by Henry 'Pi' James
  6. # now not so much.
  7. # see LICENSE.txt for license information
  8.  
  9. from BitTorrent.download import download
  10. from BitTorrent.fmt import fmttime, fmtsize
  11. from threading import Thread, Event, Lock
  12. from os import listdir
  13. from os.path import abspath, join, exists, getsize
  14. from sys import argv, stdout, exit
  15. from time import sleep
  16. from signal import signal, SIGWINCH 
  17. import traceback
  18.  
  19.  
  20. def dummy(*args, **kwargs):
  21.     pass
  22.  
  23. threads = {}
  24. ext = '.torrent'
  25. status = 'btlaunchmany starting..'
  26. filecheck = Lock()
  27. mainquitflag = Event()
  28. winchanging = 0
  29.  
  30. def cleanup_and_quit():
  31.     status = 'Killing torrents..'
  32.     for file, threadinfo in threads.items(): 
  33.         status = 'Killing torrent %s' % file
  34.         threadinfo['kill'].set() 
  35.         threadinfo['thread'].join() 
  36.         del threads[file]
  37.     displaykiller.set()
  38.     displaythread.join()
  39.    
  40. def dropdir_mainloop(d, params):
  41.     deadfiles = []
  42.     global threads, status, mainquitflag
  43.     while 1:
  44.         files = listdir(d)
  45.         # new files
  46.         for file in files: 
  47.             if file[-len(ext):] == ext:
  48.                 if file not in threads.keys() + deadfiles:
  49.                     threads[file] = {'kill': Event(), 'try': 1}
  50.                     status = 'New torrent: %s' % file
  51.                     threads[file]['thread'] = Thread(target = StatusUpdater(join(d, file), params, file).download, name = file)
  52.                     threads[file]['thread'].start()
  53.         # files with multiple tries
  54.         for file, threadinfo in threads.items():
  55.             if threadinfo.get('timeout') == 0:
  56.                 # Zero seconds left, try and start the thing again.
  57.                 threadinfo['try'] = threadinfo['try'] + 1
  58.                 threadinfo['thread'] = Thread(target = StatusUpdater(join(d, file), params, file).download, name = file)
  59.                 threadinfo['thread'].start()
  60.                 threadinfo['timeout'] = -1
  61.             elif threadinfo.get('timeout') > 0: 
  62.                 # Decrement our counter by 1
  63.                 threadinfo['timeout'] = threadinfo['timeout'] - 1
  64.             elif not threadinfo['thread'].isAlive():
  65.                 # died without permission
  66.                 # if it was checking the file, it's not anymore
  67.                 if threadinfo.get('checking', None):
  68.                     filecheck.release()
  69.                 if threadinfo.get('try') == 6: 
  70.                     # Died on the sixth try? You're dead.
  71.                     deadfiles.append(file)
  72.                     status = '%s died 6 times, added to dead list' % file
  73.                     del threads[file]
  74.                 else:
  75.                     del threadinfo['thread']
  76.                     threadinfo['timeout'] = 10
  77.             # dealing with files that dissapear
  78.             if file not in files:
  79.                 status = 'Gone torrent: %s' % file
  80.                 if threadinfo.get('timeout', -1) == -1:
  81.                     threadinfo['kill'].set()
  82.                     threadinfo['thread'].join()
  83.                 # if it had the lock, unlock it
  84.                 if threadinfo.get('checking', None):
  85.                     filecheck.release()
  86.                 del threads[file]
  87.         for file in deadfiles:
  88.             # if the file dissapears, remove it from our dead list
  89.             if file not in files: 
  90.                 deadfiles.remove(file)
  91.         if mainquitflag.isSet(): 
  92.             cleanup_and_quit()
  93.             break
  94.         sleep(1)
  95.  
  96. def display_thread(displaykiller, mainquitflag):
  97.     interval = 0.1
  98.     global threads, status, mainwinh
  99.     while 1:
  100.       if winchanging != 1: # writing now is okay.
  101.           inchar = mainwin.getch();
  102.           if inchar == 12: # ^L
  103.               winch_handler()
  104.           elif inchar == ord('q'): # quit
  105.               mainquitflag.set() 
  106.           # display file info
  107.           if (displaykiller.isSet()): 
  108.               break
  109.           mainwin.erase()
  110.           winpos = 0
  111.           totalup = 0
  112.           totaldown = 0
  113.           totaluptotal = 0.0
  114.           totaldowntotal = 0.0
  115.           tdis = threads.items()
  116.           tdis.sort()
  117.           missed = 0
  118.           for file, threadinfo in tdis: 
  119.               if (winpos + 3) >= mainwinh: 
  120.                 missed = missed + 1
  121.                 continue
  122.               uprate = threadinfo.get('uprate', 0)
  123.               downrate = threadinfo.get('downrate', 0)
  124.               uptxt = '%s/s' % fmtsize(uprate)
  125.               downtxt = '%s/s' % fmtsize(downrate)
  126.               uptotal = threadinfo.get('uptotal', 0.0)
  127.               downtotal = threadinfo.get('downtotal', 0.0)
  128.               uptotaltxt = fmtsize(uptotal, baseunit = 2)
  129.               downtotaltxt = fmtsize(downtotal, baseunit = 2)
  130.               filesize = threadinfo.get('filesize', 'N/A')
  131.               mainwin.addnstr(winpos, 0, threadinfo.get('savefile', file), mainwinw - 28, curses.A_BOLD)
  132.               mainwin.addnstr(winpos, mainwinw - 30, filesize, 8)
  133.               mainwin.addnstr(winpos, mainwinw - 21, downtxt, 10)
  134.               mainwin.addnstr(winpos, mainwinw - 10, uptxt, 10)
  135.               winpos = winpos + 1
  136.               mainwin.addnstr(winpos, 0, '^--- ', 5) 
  137.               if threadinfo.get('timeout', 0) > 0:
  138.                   mainwin.addnstr(winpos, 6, 'Try %d: died, retrying in %d' % (threadinfo.get('try', 1), threadinfo.get('timeout')), mainwinw - 21)
  139.               else:
  140.                   mainwin.addnstr(winpos, 6, threadinfo.get('status',''), mainwinw - 21)
  141.               mainwin.addnstr(winpos, mainwinw - 21, downtotaltxt, 8)
  142.               mainwin.addnstr(winpos, mainwinw - 10, uptotaltxt, 8)
  143.               winpos = winpos + 1
  144.               totalup += uprate
  145.               totaldown += downrate
  146.               totaluptotal += uptotal
  147.               totaldowntotal += downtotal
  148.           if missed > 0:
  149.             mainwin.addnstr(winpos, 0, '(%d more)' % missed, mainwinw) 
  150.           # display statusline
  151.           statuswin.erase() 
  152.           statuswin.addnstr(0, 0, status, mainwinw)
  153.           # display totals line
  154.           totaluptxt = '%s/s' % fmtsize(totalup)
  155.           totaldowntxt = '%s/s' % fmtsize(totaldown)
  156.           totaluptotaltxt = fmtsize(totaluptotal, baseunit = 2)
  157.           totaldowntotaltxt = fmtsize(totaldowntotal, baseunit = 2)
  158.           
  159.           totalwin.erase()
  160.           totalwin.addnstr(0, mainwinw - 29, 'Totals:', 7);
  161.           totalwin.addnstr(0, mainwinw - 21, totaldowntxt, 10)
  162.           totalwin.addnstr(0, mainwinw - 10, totaluptxt, 10)
  163.           totalwin.addnstr(1, mainwinw - 21, totaldowntotaltxt, 8)
  164.           totalwin.addnstr(1, mainwinw - 10, totaluptotaltxt, 8)
  165.           curses.panel.update_panels()
  166.           curses.doupdate()
  167.       sleep(interval)
  168.  
  169. class StatusUpdater:
  170.     def __init__(self, file, params, name):
  171.         self.file = file
  172.         self.params = params
  173.         self.name = name
  174.         self.myinfo = threads[name]
  175.         self.done = 0
  176.         self.checking = 0
  177.         self.activity = 'starting up...'
  178.         self.display()
  179.         self.myinfo['errors'] = []
  180.  
  181.     def download(self): 
  182.         download(self.params + ['--responsefile', self.file], self.choose, self.display, self.finished, self.err, self.myinfo['kill'], 80)
  183.         status = 'Torrent %s stopped' % self.file
  184.  
  185.     def finished(self): 
  186.         self.done = 1
  187.         self.myinfo['done'] = 1
  188.         self.activity = 'download succeeded!'
  189.         self.display({'fractionDone' : 1, 'downRate' : 0})
  190.  
  191.     def err(self, msg): 
  192.         self.myinfo['errors'].append(msg)
  193.         # errors often come with evil tracebacks that mess up our screen.
  194.         winch_handler()
  195.         self.display()
  196.  
  197.     def failed(self): 
  198.         self.activity = 'download failed!' 
  199.         self.display() 
  200.  
  201.     def choose(self, default, size, saveas, dir):
  202.         global filecheck
  203.         self.myinfo['downfile'] = default
  204.         self.myinfo['filesize'] = fmtsize(size)
  205.         if saveas == '': 
  206.             saveas = default
  207.         # it asks me where I want to save it before checking the file.. 
  208.         if exists(saveas) and (getsize(saveas) > 0):
  209.             # file will get checked
  210.             while (not filecheck.acquire(0) and not self.myinfo['kill'].isSet()):
  211.                 self.myinfo['status'] = 'Waiting for disk check...'
  212.                 sleep(0.1)
  213.             if not self.myinfo['kill'].isSet():
  214.                 self.checking = 1
  215.                 self.myinfo['checking'] = 1
  216.         self.myinfo['savefile'] = saveas
  217.         return saveas
  218.     
  219.     def display(self, dict = {}):
  220.         global winchanging
  221.         if winchanging == 1: # If we write now, we could write off the edge of the 
  222.           return             # screen and make curses mad :(
  223.         fractionDone = dict.get('fractionDone', None)
  224.         timeEst = dict.get('timeEst', None)
  225.         activity = dict.get('activity', None) 
  226.         global filecheck, status
  227.         if activity is not None and not self.done: 
  228.             self.activity = activity
  229.         elif timeEst is not None: 
  230.             self.activity = fmttime(timeEst)
  231.         if fractionDone is not None: 
  232.             self.myinfo['status'] = '%s (%.1f%%)' % (self.activity, fractionDone * 100)
  233.         else: 
  234.             self.myinfo['status'] = self.activity
  235.         if self.activity != 'checking existing file' and self.checking:
  236.             # we finished checking our files. 
  237.             filecheck.release()
  238.             self.checking = 0
  239.             self.myinfo['checking'] = 0
  240.         if dict.has_key('upRate'):
  241.             self.myinfo['uprate'] = dict['upRate']
  242.         if dict.has_key('downRate'):
  243.             self.myinfo['downrate'] = dict['downRate']
  244.         if dict.has_key('upTotal'):
  245.             self.myinfo['uptotal'] = dict['upTotal']
  246.         if dict.has_key('downTotal'):
  247.             self.myinfo['downtotal'] = dict['downTotal']
  248.  
  249. def prepare_display(): 
  250.     global mainwinw, scrwin, headerwin, totalwin
  251.     try:
  252.         scrwin.border(ord('|'),ord('|'),ord('-'),ord('-'),ord(' '),ord(' '),ord(' '),ord(' '))
  253.         headerwin.addnstr(0, 0, 'Filename', mainwinw - 25, curses.A_BOLD)
  254.         headerwin.addnstr(0, mainwinw - 26, 'Size', 4);
  255.         headerwin.addnstr(0, mainwinw - 19, 'Download', 8);
  256.         headerwin.addnstr(0, mainwinw -  6, 'Upload', 6);
  257.         totalwin.addnstr(0, mainwinw - 27, 'Totals:', 7);
  258.     except curses.error:
  259.         pass
  260.     mainwin.nodelay(1)
  261.     curses.panel.update_panels()
  262.     curses.doupdate()
  263.  
  264.  
  265.  
  266. def winch_handler(signum = SIGWINCH, stackframe = None): 
  267.     global scrwin, mainwin, mainwinw, headerwin, totalwin, statuswin
  268.     global scrpan, mainpan, headerpan, totalpan, statuspan
  269.     global winchanging
  270.     winchanging = 1
  271.     # SIGWINCH. Remake the frames!
  272.     ## Curses Trickery
  273.     curses.endwin()
  274.     scrwin.refresh()
  275.     scrwin = curses.newwin(0, 0, 0, 0)
  276.     make_curses_window()
  277.     winchanging = 0
  278.  
  279. def make_curses_window(): 
  280.     global scrwin, mainwin, mainwinw, headerwin, totalwin, statuswin
  281.     global scrpan, mainpan, headerpan, totalpan, statuspan, mainwinh
  282.     scrh, scrw = scrwin.getmaxyx()
  283.     scrpan = curses.panel.new_panel(scrwin)
  284.     mainwinh = scrh - 5  # - 2 (bars) - 1 (debugwin) - 1 (borderwin) - 1 (totalwin)
  285.     mainwinw = scrw - 4  # - 2 (bars) - 2 (spaces)
  286.     mainwiny = 2         # + 1 (bar) + 1 (titles)
  287.     mainwinx = 2         # + 1 (bar) + 1 (space)
  288.     # + 1 to all windows so we can write at mainwinw
  289.     mainwin = curses.newwin(mainwinh, mainwinw+1, mainwiny, mainwinx)
  290.     mainpan = curses.panel.new_panel(mainwin)
  291.  
  292.     headerwin = curses.newwin(1, mainwinw+1, 1, mainwinx)
  293.     headerpan = curses.panel.new_panel(headerwin)
  294.  
  295.     totalwin = curses.newwin(2, mainwinw+1, scrh-4, mainwinx)
  296.     totalpan = curses.panel.new_panel(totalwin)
  297.  
  298.     statuswin = curses.newwin(1, mainwinw+1, scrh-2, mainwinx)
  299.     statuspan = curses.panel.new_panel(statuswin)
  300.     mainwin.scrollok(0)
  301.     headerwin.scrollok(0)
  302.     totalwin.scrollok(0)
  303.     statuswin.addstr(0, 0, 'window size: %s x %s' % (scrw, scrh))
  304.     statuswin.scrollok(0)
  305.     prepare_display()
  306.  
  307.  
  308.  
  309. if __name__ == '__main__':
  310.     if (len(argv) < 2):
  311.         print """Usage: btlaunchmanycurses.py <directory> <global options>
  312.   <directory> - directory to look for .torrent files (non-recursive)
  313.   <global options> - options to be applied to all torrents (see btdownloadheadless.py)
  314. """
  315.         exit(-1)
  316.     dietrace = 0
  317.     try:
  318.         import curses
  319.         import curses.panel
  320.         scrwin = curses.initscr()
  321.         curses.noecho()
  322.         curses.cbreak()
  323.     except:
  324.         print 'Textmode GUI initialization failed, cannot proceed.'
  325.         exit(-1)
  326.     try:
  327.         signal(SIGWINCH, winch_handler)
  328.         make_curses_window()
  329.         displaykiller = Event()
  330.         displaythread = Thread(target = display_thread, name = 'display', args = [displaykiller, mainquitflag])
  331.         displaythread.setDaemon(1)
  332.         displaythread.start()
  333.         dropdir_mainloop(argv[1], argv[2:])
  334.     except KeyboardInterrupt: 
  335.         cleanup_and_quit()
  336.     except:
  337.         dietrace = traceback
  338.     curses.nocbreak()
  339.     curses.echo()
  340.     curses.endwin()
  341.     if dietrace != 0:
  342.         dietrace.print_exc()
  343.