|
version 1.1, 2003/09/23 22:31:07
|
version 1.2, 2003/09/23 22:37:27
|
|
|
|
| #!/usr/bin/env python | #!/usr/bin/env python |
| | |
| # Written by Michael Janssen (jamuraa at base0 dot net) | # Written by Michael Janssen (jamuraa at base0 dot net) |
| # heavily borrowed code from btlaunchmany.py written by Bram Cohen |
# originally heavily borrowed code from btlaunchmany.py by Bram Cohen |
| # and btdownloadcurses.py written by Henry 'Pi' James | # and btdownloadcurses.py written by Henry 'Pi' James |
| # fmttime and fmtsize mercilessly stolen from btdownloadcurses. 0% of them are mine. |
# now not so much. |
| |
# fmttime and fmtsize stolen from btdownloadcurses. |
| # see LICENSE.txt for license information | # see LICENSE.txt for license information |
| | |
| from BitTorrent.download import download | from BitTorrent.download import download |
| from threading import Thread, Event |
from threading import Thread, Event, RLock |
| from os import listdir | from os import listdir |
| from os.path import abspath, join, exists | from os.path import abspath, join, exists |
| from sys import argv, version, stdout, exit | from sys import argv, version, stdout, exit |
|
|
|
| | |
| def fmttime(n): | def fmttime(n): |
| if n == -1: | if n == -1: |
| return 'download not progressing (file not being uploaded by others?)' |
return 'download not progressing (no seeds?)' |
| if n == 0: | if n == 0: |
| return 'download complete!' | return 'download complete!' |
| n = int(n) | n = int(n) |
|
|
|
| return 'n/a' | return 'n/a' |
| return 'finishing in %d:%02d:%02d' % (h, m, s) | return 'finishing in %d:%02d:%02d' % (h, m, s) |
| | |
| |
|
| def fmtsize(n): | def fmtsize(n): |
| unit = [' B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] |
unit = [' B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'] |
| i = 0 | i = 0 |
| if (n > 999): | if (n > 999): |
| i = 1 | i = 1 |
|
|
|
| def dummy(*args, **kwargs): | def dummy(*args, **kwargs): |
| pass | pass |
| | |
| def winch_handler(signum, stackframe): |
threads = {} |
| global scrwin, mainwin, mainwinw, headerwin, totalwin, statuswin |
|
| global scrpan, mainpan, headerpan, totalpan, statuspan |
|
| # SIGWINCH. Remake the frames! |
|
| ## Curses Trickery |
|
| curses.endwin() |
|
| # delete scrwin somehow? |
|
| scrwin.refresh() |
|
| scrwin = curses.newwin(0, 0, 0, 0) |
|
| scrh, scrw = scrwin.getmaxyx() |
|
| scrpan = curses.panel.new_panel(scrwin) |
|
| ### Curses Setup |
|
| 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(1, mainwinw+1, scrh-3, 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 resize: %s x %s' % (scrw, scrh)) |
|
| statuswin.scrollok(0) |
|
| prepare_display() |
|
| |
|
| ext = '.torrent' | ext = '.torrent' |
| wininfo = {} |
status = 'btlaunchmany starting..' |
| |
filecheck = RLock() |
| | |
| def runmany(d, params): |
def dropdir_mainloop(d, params): |
| threads = [] |
|
| deadfiles = [] | deadfiles = [] |
| try: |
global threads, status |
| while 1: | while 1: |
| files = listdir(d) | files = listdir(d) |
| # new files | # new files |
| for file in files: | for file in files: |
| if file[-len(ext):] == ext: | if file[-len(ext):] == ext: |
| if file not in [x.getName() for x in threads] + deadfiles: |
if file not in threads.keys() + deadfiles: |
| wininfo[file] = {'basex': 2 * len(threads), 'killflag': Event()} |
threads[file] = {'kill': Event(), 'try': 1} |
| statuswin.erase() |
status = 'New torrent: %s' % file |
| statuswin.addnstr(0, 0,'new torrent detected: %s' % file, mainwinw) |
threads[file]['thread'] = Thread(target = StatusUpdater(join(d, file), params, file).download, name = file) |
| threads.append(Thread(target = SingleCursesDisplayer(join(d, file), params, file).download, name = file)) |
threads[file]['thread'].start() |
| threads[-1].start() |
# files with multiple tries |
| # gone files |
for file, threadinfo in threads.items(): |
| for i in range(len(threads)): |
if threadinfo.get('timeout') == 0: |
| try: |
# Zero seconds left, try and start the thing again. |
| threadname = threads[i].getName() |
threadinfo['try'] = threadinfo['try'] + 1 |
| except IndexError: |
threadinfo['thread'] = Thread(target = StatusUpdater(join(d, file), params, file).download, name = file) |
| # raised when we delete a thread from earlier, so the last ones fall out of range |
threadinfo['thread'].start() |
| continue |
threadinfo['timeout'] = -1 |
| if not threads[i].isAlive(): |
elif threadinfo.get('timeout') > 0: |
| # died without "permission" |
# Decrement our counter by 1 |
| deadfiles.append(threadname) |
threadinfo['timeout'] = threadinfo['timeout'] - 1 |
| statuswin.erase() |
elif not threadinfo['thread'].isAlive(): |
| statuswin.addnstr(0, 0,'torrent died: %s' % threadname, mainwinw) |
# died without permission |
| |
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 |
| |
threadinfo['kill'].set() |
| |
threadinfo['thread'].join() |
| |
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) |
| |
sleep(1) |
| | |
| # rearrange remaining windows |
def display_thread(displaykiller): |
| mainwin.addnstr(wininfo[threadname]['basex'], 0, ' ' * mainwinw, mainwinw) |
interval = 0.1 |
| mainwin.addnstr(wininfo[threadname]['basex']+1, 0, ' ' * mainwinw, mainwinw) |
global threads, status |
| for _, win in wininfo.items(): |
while 1: |
| if win['basex'] > wininfo[threadname]['basex']: |
# display file info |
| win['basex'] = win['basex'] - 2 |
if (displaykiller.isSet()): |
| del wininfo[threadname] |
break |
| del threads[i] |
mainwin.erase() |
| elif threadname not in files: |
winpos = 0 |
| wininfo[threadname]['killflag'].set() |
|
| # rearrange remaining windows |
|
| mainwin.addnstr(wininfo[threadname]['basex'], 0, ' ' * mainwinw, mainwinw) |
|
| mainwin.addnstr(wininfo[threadname]['basex']+1, 0, ' ' * mainwinw, mainwinw) |
|
| for _, win in wininfo.items(): |
|
| if win['basex'] > wininfo[threadname]['basex']: |
|
| win['basex'] = win['basex'] - 2 |
|
| threads[i].join() |
|
| del wininfo[threadname] |
|
| del threads[i] |
|
| # update the totals |
|
| totalup = 0 | totalup = 0 |
| totaldown = 0 | totaldown = 0 |
| for info in wininfo.values(): |
for file, threadinfo in threads.items(): |
| totalup += info.get('uprate', 0) |
uprate = threadinfo.get('uprate', 0) |
| totaldown += info.get('downrate', 0) |
downrate = threadinfo.get('downrate', 0) |
| stringup = '%s/s' % fmtsize(totalup) |
uptxt = '%s/s' % fmtsize(uprate) |
| stringdown = '%s/s' % fmtsize(totaldown) |
downtxt = '%s/s' % fmtsize(downrate) |
| |
filesize = threadinfo.get('filesize', 'N/A') |
| totalwin.addnstr(0, mainwinw-20, ' ' * 20, 20) |
mainwin.addnstr(winpos, 0, threadinfo.get('savefile', file), mainwinw - 28, curses.A_BOLD) |
| totalwin.addnstr(0, mainwinw-20 + (10 - len(stringdown)), stringdown, 10) |
mainwin.addnstr(winpos, mainwinw - 28 + (8 - len(filesize)), filesize, 8) |
| totalwin.addnstr(0, mainwinw-10 + (10 - len(stringup)), stringup, 10) |
mainwin.addnstr(winpos, mainwinw - 20 + (10 - len(downtxt)), downtxt, 10) |
| |
mainwin.addnstr(winpos, mainwinw - 10 + (10 - len(uptxt)), uptxt, 10) |
| sleep(1) |
winpos = winpos + 1 |
| except KeyboardInterrupt: |
mainwin.addnstr(winpos, 0, '^--- ', 5) |
| statuswin.erase() |
if threadinfo.get('timeout', 0) > 0: |
| statuswin.addnstr(0, 0,'^C caught.. cleaning up.. ', mainwinw) |
mainwin.addnstr(winpos, 6, 'Try %d: died, retrying in %d' % (threadinfo.get('try', 1), threadinfo.get('timeout')), mainwinw - 5) |
| curses.panel.update_panels() |
else: |
| curses.doupdate() |
mainwin.addnstr(winpos, 6, threadinfo.get('status',''), mainwinw - 5) |
| for thread in threads: |
winpos = winpos + 1 |
| threadname = thread.getName() |
totalup += uprate |
| statuswin.erase() |
totaldown += downrate |
| statuswin.addnstr(0, 0,'killing torrent %s' % threadname, mainwinw) |
# display statusline |
| curses.panel.update_panels() |
|
| curses.doupdate() |
|
| wininfo[threadname]['killflag'].set() |
|
| thread.join() |
|
| statuswin.erase() | statuswin.erase() |
| statuswin.addnstr(0, 0,'Bye Bye!', mainwinw) |
statuswin.addnstr(0, 0, status, mainwinw) |
| |
# display totals line |
| |
totaluptxt = '%s/s' % fmtsize(totaldown) |
| |
totaldowntxt = '%s/s' % fmtsize(totalup) |
| |
|
| |
totalwin.erase() |
| |
totalwin.addnstr(0, mainwinw - 27, 'Totals:', 7); |
| |
totalwin.addnstr(0, mainwinw - 20 + (10 - len(totaluptxt)), totaluptxt, 10) |
| |
totalwin.addnstr(0, mainwinw - 10 + (10 - len(totaldowntxt)), totaldowntxt, 10) |
| curses.panel.update_panels() | curses.panel.update_panels() |
| curses.doupdate() | curses.doupdate() |
| |
sleep(interval) |
| | |
| |
class StatusUpdater: |
| class SingleCursesDisplayer: |
|
| def __init__(self, file, params, name): | def __init__(self, file, params, name): |
| self.file = file | self.file = file |
| self.params = params | self.params = params |
| self.status = 'starting...' |
self.name = name |
| self.doingdown = '' |
self.myinfo = threads[name] |
| self.doingup = '' |
|
| self.done = 0 | self.done = 0 |
| self.downfile = '' |
self.checking = 0 |
| self.localfile = '' |
self.activity = 'starting up...' |
| self.fileSize = '' |
|
| self.activity = '' |
|
| self.myname = name |
|
| self.basex = wininfo[self.myname]['basex'] |
|
| self.display() | self.display() |
| |
self.myinfo['errors'] = [] |
| | |
| def download(self): | def download(self): |
| download(self.params + ['--responsefile', self.file], self.choose, self.display, self.finished, self.err, wininfo[self.myname]['killflag'], mainwinw) |
download(self.params + ['--responsefile', self.file], self.choose, self.display, self.finished, self.err, self.myinfo['kill'], 80) |
| statuswin.erase(); |
status = 'Torrent %s stopped' % self.file |
| statuswin.addnstr(0, 0, '%s: torrent stopped' % self.localfile, mainwinw) |
|
| curses.panel.update_panels() |
|
| curses.doupdate() |
|
| | |
| def finished(self): | def finished(self): |
| self.done = 1 | self.done = 1 |
| self.doingdown = '--- KB/s' |
self.myinfo['done'] = 1 |
| self.activity = 'download succeeded!' | self.activity = 'download succeeded!' |
| self.display(fractionDone = 1) | self.display(fractionDone = 1) |
| | |
| def err(self, msg): | def err(self, msg): |
| self.status = msg |
self.myinfo['errors'].append(msg) |
| self.display() | self.display() |
| | |
| def failed(self): | def failed(self): |
|
|
|
| self.display() | self.display() |
| | |
| def choose(self, default, size, saveas, dir): | def choose(self, default, size, saveas, dir): |
| self.downfile = default |
global filecheck |
| self.fileSize = fmtsize(size) |
self.myinfo['downfile'] = default |
| |
self.myinfo['filesize'] = fmtsize(size) |
| if saveas == '': | if saveas == '': |
| saveas = default | saveas = default |
| self.localfile = abspath(saveas) |
# it asks me where I want to save it before checking the file.. |
| |
if (exists(saveas)): |
| |
# file will get checked |
| |
while (not filecheck.acquire(blocking = 0) and not self.myinfo['kill'].isSet()): |
| |
self.myinfo['status'] = 'Waiting for disk check...' |
| |
sleep(0.1) |
| |
self.checking = 1 |
| |
self.myinfo['savefile'] = saveas |
| return saveas | return saveas |
| | |
| def display(self, fractionDone = None, timeEst = None, downRate = None, upRate = None, activity = None): |
def display(self, fractionDone = None, timeEst = None, downRate = None, upRate = None, activity = None, statistics = None, **kws): |
| if self.basex != wininfo[self.myname]['basex']: |
global filecheck, status |
| # leave nothing but blank space |
|
| mainwin.addnstr(self.basex, 0, ' ' * 1000, mainwinw) |
|
| mainwin.addnstr(self.basex+1, 0, ' ' * 1000, mainwinw) |
|
| self.basex = wininfo[self.myname]['basex'] |
|
| if activity is not None and not self.done: | if activity is not None and not self.done: |
| self.activity = activity | self.activity = activity |
| elif timeEst is not None: | elif timeEst is not None: |
| self.activity = fmttime(timeEst) | self.activity = fmttime(timeEst) |
| if fractionDone is not None: | if fractionDone is not None: |
| self.status = '%s (%.1f%%)' % (self.activity, fractionDone * 100) |
self.myinfo['status'] = '%s (%.1f%%)' % (self.activity, fractionDone * 100) |
| |
if fractionDone == 1 and self.checking: |
| |
# we finished checking our files. |
| |
filecheck.release() |
| |
self.checking = 0 |
| else: | else: |
| self.status = self.activity |
self.myinfo['status'] = self.activity |
| if downRate is None: | if downRate is None: |
| downRate = 0 | downRate = 0 |
| if upRate is None: | if upRate is None: |
| upRate = 0 | upRate = 0 |
| wininfo[self.myname]['downrate'] = int(downRate) |
self.myinfo['uprate'] = int(upRate) |
| wininfo[self.myname]['uprate'] = int(upRate) |
self.myinfo['downrate'] = int(downRate) |
| self.doingdown = '%s/s' % fmtsize(int(downRate)) |
|
| self.doingup = '%s/s' % fmtsize(int(upRate)) |
|
| |
|
| # clear the stats section |
|
| mainwin.addnstr(self.basex, 0, ' ' * mainwinw, mainwinw) |
|
| mainwin.addnstr(self.basex, 0, self.downfile, mainwinw - 28, curses.A_BOLD) |
|
| mainwin.addnstr(self.basex, mainwinw - 28 + (8 - len(self.fileSize)), self.fileSize, 8) |
|
| mainwin.addnstr(self.basex, mainwinw - 20 + (10 - len(self.doingdown)), self.doingdown, 10) |
|
| mainwin.addnstr(self.basex, mainwinw - 10 + (10 - len(self.doingup)), self.doingup, 10) |
|
| # clear the status bar first |
|
| mainwin.addnstr(self.basex+1, 0, ' ' * mainwinw, mainwinw) |
|
| mainwin.addnstr(self.basex+1, 0, '^--- ', 5) |
|
| mainwin.addnstr(self.basex+1, 6, self.status, (mainwinw-1) - 5) |
|
| curses.panel.update_panels() |
|
| curses.doupdate() |
|
| | |
| def prepare_display(): | def prepare_display(): |
| global mainwinw, scrwin, headerwin, totalwin | global mainwinw, scrwin, headerwin, totalwin |
|
|
|
| headerwin.addnstr(0, mainwinw - 24, 'Size', 4); | headerwin.addnstr(0, mainwinw - 24, 'Size', 4); |
| headerwin.addnstr(0, mainwinw - 18, 'Download', 8); | headerwin.addnstr(0, mainwinw - 18, 'Download', 8); |
| headerwin.addnstr(0, mainwinw - 6, 'Upload', 6); | headerwin.addnstr(0, mainwinw - 6, 'Upload', 6); |
| |
|
| totalwin.addnstr(0, mainwinw - 27, 'Totals:', 7); | totalwin.addnstr(0, mainwinw - 27, 'Totals:', 7); |
| |
|
| curses.panel.update_panels() | curses.panel.update_panels() |
| curses.doupdate() | curses.doupdate() |
| | |
| |
def winch_handler(signum, stackframe): |
| |
global scrwin, mainwin, mainwinw, headerwin, totalwin, statuswin |
| |
global scrpan, mainpan, headerpan, totalpan, statuspan |
| |
# SIGWINCH. Remake the frames! |
| |
## Curses Trickery |
| |
curses.endwin() |
| |
# delete scrwin somehow? |
| |
scrwin.refresh() |
| |
scrwin = curses.newwin(0, 0, 0, 0) |
| |
scrh, scrw = scrwin.getmaxyx() |
| |
scrpan = curses.panel.new_panel(scrwin) |
| |
### Curses Setup |
| |
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(1, mainwinw+1, scrh-3, 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 resize: %s x %s' % (scrw, scrh)) |
| |
statuswin.scrollok(0) |
| |
prepare_display() |
| |
|
| if __name__ == '__main__': | if __name__ == '__main__': |
| if (len(argv) < 2): | if (len(argv) < 2): |
| print """Usage: btlaunchmanycurses.py <directory> <global options> | print """Usage: btlaunchmanycurses.py <directory> <global options> |
|
|
|
| try: | try: |
| import curses | import curses |
| import curses.panel | import curses.panel |
| |
|
| scrwin = curses.initscr() | scrwin = curses.initscr() |
| curses.noecho() | curses.noecho() |
| curses.cbreak() | curses.cbreak() |
|
|
|
| print 'Textmode GUI initialization failed, cannot proceed.' | print 'Textmode GUI initialization failed, cannot proceed.' |
| exit(-1) | exit(-1) |
| try: | try: |
| try: |
|
| signal(SIGWINCH, winch_handler) | signal(SIGWINCH, winch_handler) |
| ### Curses Setup | ### Curses Setup |
| scrh, scrw = scrwin.getmaxyx() | scrh, scrw = scrwin.getmaxyx() |
|
|
|
| statuswin.addstr(0, 0, 'btlaunchmany started') | statuswin.addstr(0, 0, 'btlaunchmany started') |
| statuswin.scrollok(0) | statuswin.scrollok(0) |
| prepare_display() | prepare_display() |
| curses.panel.update_panels() |
displaykiller = Event() |
| curses.doupdate() |
displaythread = Thread(target = display_thread, name = 'display', args = [displaykiller]) |
| runmany(argv[1], argv[2:]) |
displaythread.setDaemon(1) |
| finally: |
displaythread.start() |
| |
dropdir_mainloop(argv[1], argv[2:]) |
| |
except KeyboardInterrupt: |
| |
status = '^C caught! 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() |
| curses.nocbreak() | curses.nocbreak() |
| curses.echo() | curses.echo() |
| curses.endwin() | curses.endwin() |
| except: | except: |
| |
curses.nocbreak() |
| |
curses.echo() |
| |
curses.endwin() |
| traceback.print_exc() | traceback.print_exc() |