home *** CD-ROM | disk | FTP | other *** search
- #! /usr/bin/python
-
- # Written by Michael Janssen (jamuraa at base0 dot net)
- # originally heavily borrowed code from btlaunchmany.py by Bram Cohen
- # and btdownloadcurses.py written by Henry 'Pi' James
- # now not so much.
- # see LICENSE.txt for license information
-
- from BitTorrent.download import download
- from BitTorrent.fmt import fmttime, fmtsize
- from threading import Thread, Event, Lock
- from os import listdir
- from os.path import abspath, join, exists, getsize
- from sys import argv, stdout, exit
- from time import sleep
- from signal import signal, SIGWINCH
- import traceback
-
-
- def dummy(*args, **kwargs):
- pass
-
- threads = {}
- ext = '.torrent'
- status = 'btlaunchmany starting..'
- filecheck = Lock()
- mainquitflag = Event()
- winchanging = 0
-
- def cleanup_and_quit():
- status = 'Killing torrents..'
- for file, threadinfo in threads.items():
- status = 'Killing torrent %s' % file
- threadinfo['kill'].set()
- threadinfo['thread'].join()
- del threads[file]
- displaykiller.set()
- displaythread.join()
-
- def dropdir_mainloop(d, params):
- deadfiles = []
- global threads, status, mainquitflag
- while 1:
- files = listdir(d)
- # new files
- for file in files:
- if file[-len(ext):] == ext:
- if file not in threads.keys() + deadfiles:
- threads[file] = {'kill': Event(), 'try': 1}
- status = 'New torrent: %s' % file
- threads[file]['thread'] = Thread(target = StatusUpdater(join(d, file), params, file).download, name = file)
- threads[file]['thread'].start()
- # files with multiple tries
- for file, threadinfo in threads.items():
- if threadinfo.get('timeout') == 0:
- # Zero seconds left, try and start the thing again.
- threadinfo['try'] = threadinfo['try'] + 1
- threadinfo['thread'] = Thread(target = StatusUpdater(join(d, file), params, file).download, name = file)
- threadinfo['thread'].start()
- threadinfo['timeout'] = -1
- elif threadinfo.get('timeout') > 0:
- # Decrement our counter by 1
- threadinfo['timeout'] = threadinfo['timeout'] - 1
- elif not threadinfo['thread'].isAlive():
- # died without permission
- # if it was checking the file, it's not anymore
- if threadinfo.get('checking', None):
- filecheck.release()
- if threadinfo.get('try') == 6:
- # Died on the sixth try? You're dead.
- deadfiles.append(file)
- status = '%s died 6 times, added to dead list' % file
- del threads[file]
- else:
- del threadinfo['thread']
- threadinfo['timeout'] = 10
- # dealing with files that dissapear
- if file not in files:
- status = 'Gone torrent: %s' % file
- if threadinfo.get('timeout', -1) == -1:
- threadinfo['kill'].set()
- threadinfo['thread'].join()
- # if it had the lock, unlock it
- if threadinfo.get('checking', None):
- filecheck.release()
- del threads[file]
- for file in deadfiles:
- # if the file dissapears, remove it from our dead list
- if file not in files:
- deadfiles.remove(file)
- if mainquitflag.isSet():
- cleanup_and_quit()
- break
- sleep(1)
-
- def display_thread(displaykiller, mainquitflag):
- interval = 0.1
- global threads, status, mainwinh
- while 1:
- if winchanging != 1: # writing now is okay.
- inchar = mainwin.getch();
- if inchar == 12: # ^L
- winch_handler()
- elif inchar == ord('q'): # quit
- mainquitflag.set()
- # display file info
- if (displaykiller.isSet()):
- break
- mainwin.erase()
- winpos = 0
- totalup = 0
- totaldown = 0
- totaluptotal = 0.0
- totaldowntotal = 0.0
- tdis = threads.items()
- tdis.sort()
- missed = 0
- for file, threadinfo in tdis:
- if (winpos + 3) >= mainwinh:
- missed = missed + 1
- continue
- uprate = threadinfo.get('uprate', 0)
- downrate = threadinfo.get('downrate', 0)
- uptxt = '%s/s' % fmtsize(uprate)
- downtxt = '%s/s' % fmtsize(downrate)
- uptotal = threadinfo.get('uptotal', 0.0)
- downtotal = threadinfo.get('downtotal', 0.0)
- uptotaltxt = fmtsize(uptotal, baseunit = 2)
- downtotaltxt = fmtsize(downtotal, baseunit = 2)
- filesize = threadinfo.get('filesize', 'N/A')
- mainwin.addnstr(winpos, 0, threadinfo.get('savefile', file), mainwinw - 28, curses.A_BOLD)
- mainwin.addnstr(winpos, mainwinw - 30, filesize, 8)
- mainwin.addnstr(winpos, mainwinw - 21, downtxt, 10)
- mainwin.addnstr(winpos, mainwinw - 10, uptxt, 10)
- winpos = winpos + 1
- mainwin.addnstr(winpos, 0, '^--- ', 5)
- if threadinfo.get('timeout', 0) > 0:
- mainwin.addnstr(winpos, 6, 'Try %d: died, retrying in %d' % (threadinfo.get('try', 1), threadinfo.get('timeout')), mainwinw - 21)
- else:
- mainwin.addnstr(winpos, 6, threadinfo.get('status',''), mainwinw - 21)
- mainwin.addnstr(winpos, mainwinw - 21, downtotaltxt, 8)
- mainwin.addnstr(winpos, mainwinw - 10, uptotaltxt, 8)
- winpos = winpos + 1
- totalup += uprate
- totaldown += downrate
- totaluptotal += uptotal
- totaldowntotal += downtotal
- if missed > 0:
- mainwin.addnstr(winpos, 0, '(%d more)' % missed, mainwinw)
- # display statusline
- statuswin.erase()
- statuswin.addnstr(0, 0, status, mainwinw)
- # display totals line
- totaluptxt = '%s/s' % fmtsize(totalup)
- totaldowntxt = '%s/s' % fmtsize(totaldown)
- totaluptotaltxt = fmtsize(totaluptotal, baseunit = 2)
- totaldowntotaltxt = fmtsize(totaldowntotal, baseunit = 2)
-
- totalwin.erase()
- totalwin.addnstr(0, mainwinw - 29, 'Totals:', 7);
- totalwin.addnstr(0, mainwinw - 21, totaldowntxt, 10)
- totalwin.addnstr(0, mainwinw - 10, totaluptxt, 10)
- totalwin.addnstr(1, mainwinw - 21, totaldowntotaltxt, 8)
- totalwin.addnstr(1, mainwinw - 10, totaluptotaltxt, 8)
- curses.panel.update_panels()
- curses.doupdate()
- sleep(interval)
-
- class StatusUpdater:
- def __init__(self, file, params, name):
- self.file = file
- self.params = params
- self.name = name
- self.myinfo = threads[name]
- self.done = 0
- self.checking = 0
- self.activity = 'starting up...'
- self.display()
- self.myinfo['errors'] = []
-
- def download(self):
- download(self.params + ['--responsefile', self.file], self.choose, self.display, self.finished, self.err, self.myinfo['kill'], 80)
- status = 'Torrent %s stopped' % self.file
-
- def finished(self):
- self.done = 1
- self.myinfo['done'] = 1
- self.activity = 'download succeeded!'
- self.display({'fractionDone' : 1, 'downRate' : 0})
-
- def err(self, msg):
- self.myinfo['errors'].append(msg)
- # errors often come with evil tracebacks that mess up our screen.
- winch_handler()
- self.display()
-
- def failed(self):
- self.activity = 'download failed!'
- self.display()
-
- def choose(self, default, size, saveas, dir):
- global filecheck
- self.myinfo['downfile'] = default
- self.myinfo['filesize'] = fmtsize(size)
- if saveas == '':
- saveas = default
- # it asks me where I want to save it before checking the file..
- if exists(saveas) and (getsize(saveas) > 0):
- # file will get checked
- while (not filecheck.acquire(0) and not self.myinfo['kill'].isSet()):
- self.myinfo['status'] = 'Waiting for disk check...'
- sleep(0.1)
- if not self.myinfo['kill'].isSet():
- self.checking = 1
- self.myinfo['checking'] = 1
- self.myinfo['savefile'] = saveas
- return saveas
-
- def display(self, dict = {}):
- global winchanging
- if winchanging == 1: # If we write now, we could write off the edge of the
- return # screen and make curses mad :(
- fractionDone = dict.get('fractionDone', None)
- timeEst = dict.get('timeEst', None)
- activity = dict.get('activity', None)
- global filecheck, status
- if activity is not None and not self.done:
- self.activity = activity
- elif timeEst is not None:
- self.activity = fmttime(timeEst)
- if fractionDone is not None:
- self.myinfo['status'] = '%s (%.1f%%)' % (self.activity, fractionDone * 100)
- else:
- self.myinfo['status'] = self.activity
- if self.activity != 'checking existing file' and self.checking:
- # we finished checking our files.
- filecheck.release()
- self.checking = 0
- self.myinfo['checking'] = 0
- if dict.has_key('upRate'):
- self.myinfo['uprate'] = dict['upRate']
- if dict.has_key('downRate'):
- self.myinfo['downrate'] = dict['downRate']
- if dict.has_key('upTotal'):
- self.myinfo['uptotal'] = dict['upTotal']
- if dict.has_key('downTotal'):
- self.myinfo['downtotal'] = dict['downTotal']
-
- def prepare_display():
- global mainwinw, scrwin, headerwin, totalwin
- try:
- scrwin.border(ord('|'),ord('|'),ord('-'),ord('-'),ord(' '),ord(' '),ord(' '),ord(' '))
- headerwin.addnstr(0, 0, 'Filename', mainwinw - 25, curses.A_BOLD)
- headerwin.addnstr(0, mainwinw - 26, 'Size', 4);
- headerwin.addnstr(0, mainwinw - 19, 'Download', 8);
- headerwin.addnstr(0, mainwinw - 6, 'Upload', 6);
- totalwin.addnstr(0, mainwinw - 27, 'Totals:', 7);
- except curses.error:
- pass
- mainwin.nodelay(1)
- curses.panel.update_panels()
- curses.doupdate()
-
-
-
- def winch_handler(signum = SIGWINCH, stackframe = None):
- global scrwin, mainwin, mainwinw, headerwin, totalwin, statuswin
- global scrpan, mainpan, headerpan, totalpan, statuspan
- global winchanging
- winchanging = 1
- # SIGWINCH. Remake the frames!
- ## Curses Trickery
- curses.endwin()
- scrwin.refresh()
- scrwin = curses.newwin(0, 0, 0, 0)
- make_curses_window()
- winchanging = 0
-
- def make_curses_window():
- global scrwin, mainwin, mainwinw, headerwin, totalwin, statuswin
- global scrpan, mainpan, headerpan, totalpan, statuspan, mainwinh
- scrh, scrw = scrwin.getmaxyx()
- scrpan = curses.panel.new_panel(scrwin)
- mainwinh = scrh - 5 # - 2 (bars) - 1 (debugwin) - 1 (borderwin) - 1 (totalwin)
- mainwinw = scrw - 4 # - 2 (bars) - 2 (spaces)
- mainwiny = 2 # + 1 (bar) + 1 (titles)
- mainwinx = 2 # + 1 (bar) + 1 (space)
- # + 1 to all windows so we can write at mainwinw
- mainwin = curses.newwin(mainwinh, mainwinw+1, mainwiny, mainwinx)
- mainpan = curses.panel.new_panel(mainwin)
-
- headerwin = curses.newwin(1, mainwinw+1, 1, mainwinx)
- headerpan = curses.panel.new_panel(headerwin)
-
- totalwin = curses.newwin(2, mainwinw+1, scrh-4, mainwinx)
- totalpan = curses.panel.new_panel(totalwin)
-
- statuswin = curses.newwin(1, mainwinw+1, scrh-2, mainwinx)
- statuspan = curses.panel.new_panel(statuswin)
- mainwin.scrollok(0)
- headerwin.scrollok(0)
- totalwin.scrollok(0)
- statuswin.addstr(0, 0, 'window size: %s x %s' % (scrw, scrh))
- statuswin.scrollok(0)
- prepare_display()
-
-
-
- if __name__ == '__main__':
- if (len(argv) < 2):
- print """Usage: btlaunchmanycurses.py <directory> <global options>
- <directory> - directory to look for .torrent files (non-recursive)
- <global options> - options to be applied to all torrents (see btdownloadheadless.py)
- """
- exit(-1)
- dietrace = 0
- try:
- import curses
- import curses.panel
- scrwin = curses.initscr()
- curses.noecho()
- curses.cbreak()
- except:
- print 'Textmode GUI initialization failed, cannot proceed.'
- exit(-1)
- try:
- signal(SIGWINCH, winch_handler)
- make_curses_window()
- displaykiller = Event()
- displaythread = Thread(target = display_thread, name = 'display', args = [displaykiller, mainquitflag])
- displaythread.setDaemon(1)
- displaythread.start()
- dropdir_mainloop(argv[1], argv[2:])
- except KeyboardInterrupt:
- cleanup_and_quit()
- except:
- dietrace = traceback
- curses.nocbreak()
- curses.echo()
- curses.endwin()
- if dietrace != 0:
- dietrace.print_exc()
-