3
# Written by Henry 'Pi' James
4
# see LICENSE.txt for license information
8
from BitTornado import PSYCO
12
assert psyco.__version__ >= 0x010100f0
17
from BitTornado.download_bt1 import BT1Download, defaults, parse_params, get_usage, get_response
18
from BitTornado.RawServer import RawServer, UPnP_ERROR
19
from random import seed
20
from socket import error as socketerror
21
from BitTornado.bencode import bencode
22
from BitTornado.natpunch import UPnP_test
23
from threading import Event
24
from os.path import abspath
25
from signal import signal, SIGWINCH
27
from sys import argv, exit
29
from time import time, strftime
30
from BitTornado.clock import clock
31
from BitTornado import createPeerID, version
32
from BitTornado.ConfigDir import ConfigDir
37
from curses.wrapper import wrapper as curses_wrapper
38
from signal import signal, SIGWINCH
40
print 'Textmode GUI initialization failed, cannot proceed.'
42
print 'This download interface requires the standard Python module ' \
43
'"curses", which is unfortunately not available for the native ' \
44
'Windows port of Python. It is however available for the Cygwin ' \
45
'port of Python, running on all Win32 systems (www.cygwin.com).'
47
print 'You may still use "btdownloadheadless.py" to download.'
50
assert sys.version >= '2', "Install Python 2.0 or greater"
59
return 'download complete!'
62
assert n >= 0 and n < 5184000 # 60 days
67
return 'finishing in %d:%02d:%02d' % (h, m, s)
74
size = '%s,%s' % (s[-3:], size)
76
unit = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']
78
while i + 1 < len(unit) and (n >> 10) >= 999:
81
n = float(n) / (1 << 10)
82
size = '%s (%.2f %s)' % (size, n, unit[i])
86
class CursesDisplayer:
87
def __init__(self, scrwin, errlist, doneflag):
89
self.errlist = errlist
90
self.doneflag = doneflag
92
signal(SIGWINCH, self.winch_handler)
93
self.changeflag = Event()
102
self.downRate = '---'
104
self.shareRating = ''
108
self.last_update_time = 0
109
self.spew_scroll_time = 0
110
self.spew_scroll_pos = 0
112
self._remake_window()
114
def winch_handler(self, signum, stackframe):
115
self.changeflag.set()
117
self.scrwin.refresh()
118
self.scrwin = curses.newwin(0, 0, 0, 0)
119
self._remake_window()
121
def _remake_window(self):
122
self.scrh, self.scrw = self.scrwin.getmaxyx()
123
self.scrpan = curses.panel.new_panel(self.scrwin)
124
self.labelh, self.labelw, self.labely, self.labelx = 11, 9, 1, 2
125
self.labelwin = curses.newwin(self.labelh, self.labelw,
126
self.labely, self.labelx)
127
self.labelpan = curses.panel.new_panel(self.labelwin)
128
self.fieldh, self.fieldw, self.fieldy, self.fieldx = (
129
self.labelh, self.scrw-2 - self.labelw-3,
131
self.fieldwin = curses.newwin(self.fieldh, self.fieldw,
132
self.fieldy, self.fieldx)
133
self.fieldwin.nodelay(1)
134
self.fieldpan = curses.panel.new_panel(self.fieldwin)
135
self.spewh, self.speww, self.spewy, self.spewx = (
136
self.scrh - self.labelh - 2, self.scrw - 3, 1 + self.labelh, 2)
137
self.spewwin = curses.newwin(self.spewh, self.speww,
138
self.spewy, self.spewx)
139
self.spewpan = curses.panel.new_panel(self.spewwin)
141
self.scrwin.border(ord('|'),ord('|'),ord('-'),ord('-'),ord(' '),ord(' '),ord(' '),ord(' '))
144
self.labelwin.addstr(0, 0, 'file:')
145
self.labelwin.addstr(1, 0, 'size:')
146
self.labelwin.addstr(2, 0, 'dest:')
147
self.labelwin.addstr(3, 0, 'progress:')
148
self.labelwin.addstr(4, 0, 'status:')
149
self.labelwin.addstr(5, 0, 'dl speed:')
150
self.labelwin.addstr(6, 0, 'ul speed:')
151
self.labelwin.addstr(7, 0, 'sharing:')
152
self.labelwin.addstr(8, 0, 'seeds:')
153
self.labelwin.addstr(9, 0, 'peers:')
154
curses.panel.update_panels()
156
self.changeflag.clear()
161
self.activity = 'download succeeded!'
162
self.downRate = '---'
163
self.display(fractionDone = 1)
167
self.activity = 'download failed!'
168
self.downRate = '---'
171
def error(self, errormsg):
172
newerrmsg = strftime('[%H:%M:%S] ') + errormsg
173
self.errors.append(newerrmsg)
174
self.errlist.append(newerrmsg)
177
def display(self, dpflag = Event(), fractionDone = None, timeEst = None,
178
downRate = None, upRate = None, activity = None,
179
statistics = None, spew = None, **kws):
181
inchar = self.fieldwin.getch()
182
if inchar == 12: # ^L
183
self._remake_window()
184
elif inchar in (ord('q'),ord('Q')):
187
if activity is not None and not self.done:
188
self.activity = activity
189
elif timeEst is not None:
190
self.activity = fmttime(timeEst)
191
if self.changeflag.isSet():
193
if self.last_update_time + 0.1 > clock() and fractionDone not in (0.0, 1.0) and activity is not None:
195
self.last_update_time = clock()
196
if fractionDone is not None:
197
blocknum = int(self.fieldw * fractionDone)
198
self.progress = blocknum * '#' + (self.fieldw - blocknum) * '_'
199
self.status = '%s (%.1f%%)' % (self.activity, fractionDone * 100)
201
self.status = self.activity
202
if downRate is not None:
203
self.downRate = '%.1f KB/s' % (float(downRate) / (1 << 10))
204
if upRate is not None:
205
self.upRate = '%.1f KB/s' % (float(upRate) / (1 << 10))
206
if statistics is not None:
207
if (statistics.shareRating < 0) or (statistics.shareRating > 100):
208
self.shareRating = 'oo (%.1f MB up / %.1f MB down)' % (float(statistics.upTotal) / (1<<20), float(statistics.downTotal) / (1<<20))
210
self.shareRating = '%.3f (%.1f MB up / %.1f MB down)' % (statistics.shareRating, float(statistics.upTotal) / (1<<20), float(statistics.downTotal) / (1<<20))
212
self.seedStatus = '%d seen now, plus %.3f distributed copies' % (statistics.numSeeds,0.001*int(1000*statistics.numCopies2))
214
self.seedStatus = '%d seen recently, plus %.3f distributed copies' % (statistics.numOldSeeds,0.001*int(1000*statistics.numCopies))
215
self.peerStatus = '%d seen now, %.1f%% done at %.1f kB/s' % (statistics.numPeers,statistics.percentDone,float(statistics.torrentRate) / (1 << 10))
217
self.fieldwin.erase()
218
self.fieldwin.addnstr(0, 0, self.file, self.fieldw, curses.A_BOLD)
219
self.fieldwin.addnstr(1, 0, self.fileSize, self.fieldw)
220
self.fieldwin.addnstr(2, 0, self.downloadTo, self.fieldw)
222
self.fieldwin.addnstr(3, 0, self.progress, self.fieldw, curses.A_BOLD)
223
self.fieldwin.addnstr(4, 0, self.status, self.fieldw)
224
self.fieldwin.addnstr(5, 0, self.downRate, self.fieldw)
225
self.fieldwin.addnstr(6, 0, self.upRate, self.fieldw)
226
self.fieldwin.addnstr(7, 0, self.shareRating, self.fieldw)
227
self.fieldwin.addnstr(8, 0, self.seedStatus, self.fieldw)
228
self.fieldwin.addnstr(9, 0, self.peerStatus, self.fieldw)
235
self.spewwin.addnstr(0, 0, "error(s):", self.speww, curses.A_BOLD)
236
errsize = len(self.errors)
237
displaysize = min(errsize, self.spewh)
238
displaytop = errsize - displaysize
239
for i in range(displaysize):
240
self.spewwin.addnstr(i, self.labelw, self.errors[displaytop + i],
241
self.speww-self.labelw-1, curses.A_BOLD)
244
self.spewwin.addnstr(0, 0, "error:", self.speww, curses.A_BOLD)
245
self.spewwin.addnstr(0, self.labelw, self.errors[-1],
246
self.speww-self.labelw-1, curses.A_BOLD)
247
self.spewwin.addnstr(2, 0, " # IP Upload Download Completed Speed", self.speww, curses.A_BOLD)
250
if self.spew_scroll_time + SPEW_SCROLL_RATE < clock():
251
self.spew_scroll_time = clock()
252
if len(spew) > self.spewh-5 or self.spew_scroll_pos > 0:
253
self.spew_scroll_pos += 1
254
if self.spew_scroll_pos > len(spew):
255
self.spew_scroll_pos = 0
257
for i in range(len(spew)):
258
spew[i]['lineno'] = i+1
259
spew.append({'lineno': None})
260
spew = spew[self.spew_scroll_pos:] + spew[:self.spew_scroll_pos]
262
for i in range(min(self.spewh - 5, len(spew))):
263
if not spew[i]['lineno']:
265
self.spewwin.addnstr(i+3, 0, '%3d' % spew[i]['lineno'], 3)
266
self.spewwin.addnstr(i+3, 4, spew[i]['ip']+spew[i]['direction'], 16)
267
if spew[i]['uprate'] > 100:
268
self.spewwin.addnstr(i+3, 20, '%6.0f KB/s' % (float(spew[i]['uprate']) / 1000), 11)
269
self.spewwin.addnstr(i+3, 32, '-----', 5)
270
if spew[i]['uinterested'] == 1:
271
self.spewwin.addnstr(i+3, 33, 'I', 1)
272
if spew[i]['uchoked'] == 1:
273
self.spewwin.addnstr(i+3, 35, 'C', 1)
274
if spew[i]['downrate'] > 100:
275
self.spewwin.addnstr(i+3, 38, '%6.0f KB/s' % (float(spew[i]['downrate']) / 1000), 11)
276
self.spewwin.addnstr(i+3, 50, '-------', 7)
277
if spew[i]['dinterested'] == 1:
278
self.spewwin.addnstr(i+3, 51, 'I', 1)
279
if spew[i]['dchoked'] == 1:
280
self.spewwin.addnstr(i+3, 53, 'C', 1)
281
if spew[i]['snubbed'] == 1:
282
self.spewwin.addnstr(i+3, 55, 'S', 1)
283
self.spewwin.addnstr(i+3, 58, '%5.1f%%' % (float(int(spew[i]['completed']*1000))/10), 6)
284
if spew[i]['speed'] is not None:
285
self.spewwin.addnstr(i+3, 64, '%5.0f KB/s' % (float(spew[i]['speed'])/1000), 10)
287
if statistics is not None:
288
self.spewwin.addnstr(self.spewh-1, 0,
289
'downloading %d pieces, have %d fragments, %d of %d pieces completed'
290
% ( statistics.storage_active, statistics.storage_dirty,
291
statistics.storage_numcomplete,
292
statistics.storage_totalpieces ), self.speww-1 )
294
curses.panel.update_panels()
298
def chooseFile(self, default, size, saveas, dir):
300
self.fileSize = fmtsize(size)
303
self.downloadTo = abspath(saveas)
306
def run(scrwin, errlist, params):
308
d = CursesDisplayer(scrwin, errlist, doneflag)
311
configdir = ConfigDir('downloadcurses')
312
defaultsToIgnore = ['responsefile', 'url', 'priority']
313
configdir.setDefaults(defaults,defaultsToIgnore)
314
configdefaults = configdir.loadConfig()
315
defaults.append(('save_options',0,
316
"whether to save the current options as the new default configuration " +
317
"(only for btdownloadcurses.py)"))
319
config = parse_params(params, configdefaults)
320
except ValueError, e:
321
d.error('error: ' + str(e) + '\nrun with no args for parameter explanations')
324
d.error(get_usage(defaults, d.fieldw, configdefaults))
326
if config['save_options']:
327
configdir.saveConfig(config)
328
configdir.deleteOldCacheData(config['expire_cache_data'])
330
myid = createPeerID()
333
rawserver = RawServer(doneflag, config['timeout_check_interval'],
334
config['timeout'], ipv6_enable = config['ipv6_enabled'],
335
failfunc = d.failed, errorfunc = d.error)
340
listen_port = rawserver.find_and_bind(config['minport'], config['maxport'],
341
config['bind'], ipv6_socket_style = config['ipv6_binds_v4'],
342
upnp = upnp_type, randomizer = config['random_port'])
344
except socketerror, e:
345
if upnp_type and e == UPnP_ERROR:
346
d.error('WARNING: COULD NOT FORWARD VIA UPnP')
349
d.error("Couldn't listen - " + str(e))
353
response = get_response(config['responsefile'], config['url'], d.error)
357
infohash = sha(bencode(response['info'])).digest()
359
dow = BT1Download(d.display, d.finished, d.error, d.error, doneflag,
360
config, response, infohash, myid, rawserver, listen_port,
363
if not dow.saveAs(d.chooseFile):
366
if not dow.initFiles(old_style = True):
368
if not dow.startEngine():
371
dow.startRerequester()
374
if not dow.am_I_finished():
375
d.display(activity = 'connecting to peers')
376
rawserver.listen_forever(dow.getPortHandler())
377
d.display(activity = 'shutting down')
381
except KeyboardInterrupt:
392
if __name__ == '__main__':
393
if argv[1:] == ['--version']:
397
print "Usage: btdownloadcurses.py <global options>\n"
398
print get_usage(defaults)
402
curses_wrapper(run, errlist, argv[1:])
405
print "These errors occurred during execution:"
406
for error in errlist: