1 theshadow 1.1 #!/usr/bin/env python
2
|
3 theshadow 1.4 # Written by John Hoffman
|
4 theshadow 1.1 # see LICENSE.txt for license information
5
|
6 theshadow 1.9 DOWNLOAD_SCROLL_RATE = 1
7
|
8 theshadow 1.2 from BitTornado import PSYCO
|
9 theshadow 1.1 if PSYCO.psyco:
10 try:
11 import psyco
12 assert psyco.__version__ >= 0x010100f0
13 psyco.full()
14 except:
15 pass
16
|
17 theshadow 1.4 from BitTornado.launchmanycore import LaunchMany
18 from BitTornado.download_bt1 import defaults, get_usage
|
19 theshadow 1.5 from BitTornado.parseargs import parseargs
|
20 theshadow 1.4 from threading import Event
21 from sys import argv, exit
|
22 theshadow 1.10 from time import time, localtime, strftime
|
23 theshadow 1.4 import sys, os
|
24 theshadow 1.18 from BitTornado import version, report_email
|
25 theshadow 1.4
26 try:
27 import curses
28 import curses.panel
29 from curses.wrapper import wrapper as curses_wrapper
30 from signal import signal, SIGWINCH
31 except:
32 print 'Textmode GUI initialization failed, cannot proceed.'
33 print
34 print 'This download interface requires the standard Python module ' \
35 '"curses", which is unfortunately not available for the native ' \
36 'Windows port of Python. It is however available for the Cygwin ' \
37 'port of Python, running on all Win32 systems (www.cygwin.com).'
38 print
39 print 'You may still use "btdownloadheadless.py" to download.'
40 sys.exit(1)
41
42 assert sys.version >= '2', "Install Python 2.0 or greater"
43 try:
44 True
45 except:
46 theshadow 1.4 True = 1
47 False = 0
|
48 theshadow 1.1
|
49 theshadow 1.15 Exceptions = []
|
50 theshadow 1.6
|
51 theshadow 1.1 def fmttime(n):
|
52 theshadow 1.6 if n <= 0:
53 return None
|
54 theshadow 1.1 n = int(n)
55 m, s = divmod(n, 60)
56 h, m = divmod(m, 60)
57 if h > 1000000:
|
58 theshadow 1.6 return 'connecting to peers'
|
59 theshadow 1.11 return 'ETA in %d:%02d:%02d' % (h, m, s)
|
60 theshadow 1.1
61 def fmtsize(n):
|
62 theshadow 1.8 n = int(n)
|
63 theshadow 1.1 unit = [' B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
64 i = 0
65 if (n > 999):
66 i = 1
67 while i + 1 < len(unit) and (n >> 10) >= 999:
68 i += 1
69 n >>= 10
70 n = float(n) / (1 << 10)
71 if i > 0:
72 size = '%.1f' % n + '%s' % unit[i]
73 else:
74 size = '%.0f' % n + '%s' % unit[i]
75 return size
76
|
77 theshadow 1.11 def ljust(s, size):
|
78 theshadow 1.14 s = s[:size]
|
79 theshadow 1.12 return s + (' '*(size-len(s)))
|
80 theshadow 1.11
81 def rjust(s, size):
|
82 theshadow 1.14 s = s[:size]
|
83 theshadow 1.12 return (' '*(size-len(s)))+s
|
84 theshadow 1.11
|
85 theshadow 1.1
|
86 theshadow 1.4 class CursesDisplayer:
87 def __init__(self, scrwin):
|
88 theshadow 1.9 self.messages = []
|
89 theshadow 1.12 self.scroll_pos = 0
90 self.scroll_time = 0
|
91 theshadow 1.9
|
92 theshadow 1.4 self.scrwin = scrwin
93 signal(SIGWINCH, self.winch_handler)
94 self.changeflag = Event()
95 self._remake_window()
96
97 def winch_handler(self, signum, stackframe):
98 self.changeflag.set()
99 curses.endwin()
100 self.scrwin.refresh()
101 self.scrwin = curses.newwin(0, 0, 0, 0)
|
102 theshadow 1.22 self._remake_window()
|
103 theshadow 1.10 self._display_messages()
|
104 theshadow 1.4
|
105 theshadow 1.22 def _remake_window(self):
|
106 theshadow 1.4 self.scrh, self.scrw = self.scrwin.getmaxyx()
107 self.scrpan = curses.panel.new_panel(self.scrwin)
|
108 theshadow 1.9 self.mainwinh = int(2*(self.scrh)/3)
|
109 theshadow 1.4 self.mainwinw = self.scrw - 4 # - 2 (bars) - 2 (spaces)
110 self.mainwiny = 2 # + 1 (bar) + 1 (titles)
111 self.mainwinx = 2 # + 1 (bar) + 1 (space)
112 # + 1 to all windows so we can write at mainwinw
113
114 self.mainwin = curses.newwin(self.mainwinh, self.mainwinw+1,
115 self.mainwiny, self.mainwinx)
116 self.mainpan = curses.panel.new_panel(self.mainwin)
117 self.mainwin.scrollok(0)
118
119 self.headerwin = curses.newwin(1, self.mainwinw+1,
120 1, self.mainwinx)
121 self.headerpan = curses.panel.new_panel(self.headerwin)
122 self.headerwin.scrollok(0)
123
124 self.totalwin = curses.newwin(1, self.mainwinw+1,
|
125 theshadow 1.9 self.mainwinh+1, self.mainwinx)
|
126 theshadow 1.4 self.totalpan = curses.panel.new_panel(self.totalwin)
127 self.totalwin.scrollok(0)
128
|
129 theshadow 1.14 self.statuswinh = self.scrh-4-self.mainwinh
|
130 theshadow 1.11 self.statuswin = curses.newwin(self.statuswinh, self.mainwinw+1,
|
131 theshadow 1.9 self.mainwinh+3, self.mainwinx)
|
132 theshadow 1.6 self.statuspan = curses.panel.new_panel(self.statuswin)
|
133 theshadow 1.4 self.statuswin.scrollok(0)
134
135 self.scrwin.border(ord('|'),ord('|'),ord('-'),ord('-'),ord(' '),ord(' '),ord(' '),ord(' '))
|
136 theshadow 1.9 self.headerwin.addnstr(0, 2, '#', self.mainwinw - 25, curses.A_BOLD)
137 self.headerwin.addnstr(0, 4, 'Filename', self.mainwinw - 25, curses.A_BOLD)
138 self.headerwin.addnstr(0, self.mainwinw - 24, 'Size', 4, curses.A_BOLD)
139 self.headerwin.addnstr(0, self.mainwinw - 18, 'Download', 8, curses.A_BOLD)
140 self.headerwin.addnstr(0, self.mainwinw - 6, 'Upload', 6, curses.A_BOLD)
141 self.totalwin.addnstr(0, self.mainwinw - 27, 'Totals:', 7, curses.A_BOLD)
|
142 theshadow 1.22
|
143 theshadow 1.4 curses.panel.update_panels()
144 curses.doupdate()
|
145 theshadow 1.5 self.changeflag.clear()
|
146 theshadow 1.4
147
|
148 theshadow 1.11 def _display_line(self, s, bold = False):
149 if self.disp_end:
150 return True
151 line = self.disp_line
152 self.disp_line += 1
153 if line < 0:
154 return False
155 if bold:
156 self.mainwin.addnstr(line, 0, s, self.mainwinw, curses.A_BOLD)
157 else:
158 self.mainwin.addnstr(line, 0, s, self.mainwinw)
|
159 theshadow 1.14 if self.disp_line >= self.mainwinh:
|
160 theshadow 1.11 self.disp_end = True
161 return self.disp_end
162
|
163 theshadow 1.17 def _display_data(self, data):
|
164 theshadow 1.12 if 3*len(data) <= self.mainwinh:
|
165 theshadow 1.11 self.scroll_pos = 0
166 self.scrolling = False
167 elif self.scroll_time + DOWNLOAD_SCROLL_RATE < time():
168 self.scroll_time = time()
169 self.scroll_pos += 1
170 self.scrolling = True
|
171 theshadow 1.14 if self.scroll_pos >= 3*len(data)+2:
|
172 theshadow 1.11 self.scroll_pos = 0
173
174 i = int(self.scroll_pos/3)
175 self.disp_line = (3*i)-self.scroll_pos
176 self.disp_end = False
177
178 while not self.disp_end:
179 ii = i % len(data)
180 if i and not ii:
181 if not self.scrolling:
182 break
183 self._display_line('')
184 if self._display_line(''):
185 break
|
186 theshadow 1.16 ( name, status, progress, peers, seeds, seedsmsg, dist,
|
187 theshadow 1.11 uprate, dnrate, upamt, dnamt, size, t, msg ) = data[ii]
|
188 theshadow 1.6 t = fmttime(t)
189 if t:
190 status = t
|
191 theshadow 1.11 name = ljust(name,self.mainwinw-32)
192 size = rjust(fmtsize(size),8)
193 uprate = rjust('%s/s' % fmtsize(uprate),10)
194 dnrate = rjust('%s/s' % fmtsize(dnrate),10)
195 line = "%3d %s%s%s%s" % (ii+1, name, size, dnrate, uprate)
|
196 theshadow 1.17 self._display_line(line, True)
|
197 theshadow 1.9 if peers + seeds:
|
198 theshadow 1.16 datastr = ' (%s) %s - %s peers %s seeds %.3f dist copies - %s up %s dn' % (
199 progress, status, peers, seeds, dist,
|
200 theshadow 1.15 fmtsize(upamt), fmtsize(dnamt) )
|
201 theshadow 1.9 else:
|
202 theshadow 1.11 datastr = ' '+status+' ('+progress+')'
|
203 theshadow 1.17 self._display_line(datastr)
|
204 theshadow 1.11 self._display_line(' '+msg)
205 i += 1
|
206 theshadow 1.17
207 def display(self, data):
208 if self.changeflag.isSet():
209 return
210
|
211 theshadow 1.22 inchar = self.mainwin.getch()
212 if inchar == 12: # ^L
213 self._remake_window()
214
|
215 theshadow 1.17 self.mainwin.erase()
216 if data:
217 self._display_data(data)
218 else:
219 self.mainwin.addnstr( 1, int(self.mainwinw/2)-5,
220 'no torrents', 12, curses.A_BOLD )
221 totalup = 0
222 totaldn = 0
223 for ( name, status, progress, peers, seeds, seedsmsg, dist,
224 uprate, dnrate, upamt, dnamt, size, t, msg ) in data:
225 totalup += uprate
226 totaldn += dnrate
227
|
228 theshadow 1.4 totalup = '%s/s' % fmtsize(totalup)
229 totaldn = '%s/s' % fmtsize(totaldn)
|
230 theshadow 1.1
|
231 theshadow 1.4 self.totalwin.erase()
|
232 theshadow 1.9 self.totalwin.addnstr(0, self.mainwinw-27, 'Totals:', 7, curses.A_BOLD)
|
233 theshadow 1.4 self.totalwin.addnstr(0, self.mainwinw-20 + (10-len(totaldn)),
|
234 theshadow 1.11 totaldn, 10, curses.A_BOLD)
|
235 theshadow 1.4 self.totalwin.addnstr(0, self.mainwinw-10 + (10-len(totalup)),
|
236 theshadow 1.11 totalup, 10, curses.A_BOLD)
|
237 theshadow 1.17
|
238 theshadow 1.1 curses.panel.update_panels()
239 curses.doupdate()
|
240 theshadow 1.22
241 return chr(inchar) in 'qQ'
|
242 theshadow 1.1
|
243 theshadow 1.4 def message(self, s):
|
244 theshadow 1.10 self.messages.append(strftime('%x %X - ',localtime(time()))+s)
245 self._display_messages()
246
247 def _display_messages(self):
|
248 theshadow 1.15 self.statuswin.erase()
|
249 theshadow 1.9 winpos = 0
|
250 theshadow 1.12 for s in self.messages[-self.statuswinh:]:
|
251 theshadow 1.14 self.statuswin.addnstr(winpos, 0, s, self.mainwinw)
|
252 theshadow 1.9 winpos += 1
|
253 theshadow 1.10 curses.panel.update_panels()
254 curses.doupdate()
|
255 theshadow 1.15
256 def exception(self, s):
257 Exceptions.append(s)
|
258 theshadow 1.17 self.message('SYSTEM ERROR - EXCEPTION GENERATED')
|
259 theshadow 1.15
|
260 theshadow 1.4
261
262 def LaunchManyWrapper(scrwin, config):
|
263 theshadow 1.15 LaunchMany(config, CursesDisplayer(scrwin))
|
264 theshadow 1.4
|
265 theshadow 1.1
266 if __name__ == '__main__':
267 if argv[1:] == ['--version']:
268 print version
269 exit(0)
|
270 theshadow 1.20 defaults.extend( [
|
271 theshadow 1.19 ( 'parse_dir_interval', 60,
272 "how often to rescan the torrent directory, in seconds" ),
273 ( 'saveas_style', 2,
274 "How to name torrent downloads (1 = rename to torrent name, " +
|
275 theshadow 1.21 "2 = save under name in torrent, 3 = save in directory under torrent name)" ),
|
276 theshadow 1.19 ( 'display_path', 0,
|
277 theshadow 1.21 "whether to display the full path or the torrent contents for each torrent" ),
|
278 theshadow 1.20 ] )
|
279 theshadow 1.1 try:
|
280 theshadow 1.7 if len(argv) <= 1:
281 print "Usage: btlaunchmany.py <directory> <global options>\n"
282 print "<directory> - directory to look for .torrent files (semi-recursive)"
283 print get_usage(defaults)
284 exit(1)
|
285 theshadow 1.4 config, args = parseargs(argv[1:], defaults, 1, 1)
286 if not os.path.isdir(args[0]):
287 raise ValueError("Warning: "+args[0]+" is not a directory")
288 config['torrent_dir'] = args[0]
289 except ValueError, e:
290 print 'error: ' + str(e) + '\nrun with no args for parameter explanations'
291 exit(1)
|
292 theshadow 1.1
|
293 theshadow 1.15 curses_wrapper(LaunchManyWrapper, config)
294 if Exceptions:
295 print '\nEXCEPTION:'
296 print Exceptions[0]
|
297 theshadow 1.18 print 'please report this to '+report_email
|