| 1 | #!/usr/bin/env python |
|---|
| 2 | |
|---|
| 3 | # This program is free software: you can redistribute it and/or modify |
|---|
| 4 | # it under the terms of the GNU General Public License as published by |
|---|
| 5 | # the Free Software Foundation, either version 3 of the License, or |
|---|
| 6 | # (at your option) any later version. |
|---|
| 7 | # |
|---|
| 8 | # This program is distributed in the hope that it will be useful, |
|---|
| 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|---|
| 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|---|
| 11 | # GNU General Public License for more details. |
|---|
| 12 | # |
|---|
| 13 | # You should have received a copy of the GNU General Public License |
|---|
| 14 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
|---|
| 15 | |
|---|
| 16 | # Written by John Hoffman |
|---|
| 17 | |
|---|
| 18 | from __future__ import division |
|---|
| 19 | |
|---|
| 20 | DOWNLOAD_SCROLL_RATE = 1 |
|---|
| 21 | |
|---|
| 22 | import sys, os |
|---|
| 23 | from threading import Event |
|---|
| 24 | from time import time, localtime, strftime |
|---|
| 25 | |
|---|
| 26 | from Anomos.launchmanycore import LaunchMany |
|---|
| 27 | from Anomos.defaultargs import get_defaults |
|---|
| 28 | from Anomos.parseargs import parseargs, printHelp |
|---|
| 29 | from Anomos import configfile |
|---|
| 30 | from Anomos import version |
|---|
| 31 | from Anomos import BTFailure |
|---|
| 32 | |
|---|
| 33 | try: |
|---|
| 34 | import curses |
|---|
| 35 | import curses.panel |
|---|
| 36 | from curses.wrapper import wrapper as curses_wrapper |
|---|
| 37 | from signal import signal, SIGWINCH |
|---|
| 38 | except: |
|---|
| 39 | print 'Textmode GUI initialization failed, cannot proceed.' |
|---|
| 40 | print |
|---|
| 41 | print 'This download interface requires the standard Python module ' \ |
|---|
| 42 | '"curses", which is unfortunately not available for the native ' \ |
|---|
| 43 | 'Windows port of Python. It is however available for the Cygwin ' \ |
|---|
| 44 | 'port of Python, running on all Win32 systems (www.cygwin.com).' |
|---|
| 45 | print |
|---|
| 46 | print 'You may still use "anondownloadheadless.py" to download.' |
|---|
| 47 | sys.exit(1) |
|---|
| 48 | |
|---|
| 49 | exceptions = [] |
|---|
| 50 | |
|---|
| 51 | def fmttime(n): |
|---|
| 52 | if n <= 0: |
|---|
| 53 | return None |
|---|
| 54 | n = int(n) |
|---|
| 55 | m, s = divmod(n, 60) |
|---|
| 56 | h, m = divmod(m, 60) |
|---|
| 57 | if h > 1000000: |
|---|
| 58 | return 'connecting to peers' |
|---|
| 59 | return 'ETA in %d:%02d:%02d' % (h, m, s) |
|---|
| 60 | |
|---|
| 61 | def fmtsize(n): |
|---|
| 62 | n = long(n) |
|---|
| 63 | unit = [' B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'] |
|---|
| 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 /= 1024 |
|---|
| 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 | def ljust(s, size): |
|---|
| 78 | s = s[:size] |
|---|
| 79 | return s + (' '*(size-len(s))) |
|---|
| 80 | |
|---|
| 81 | def rjust(s, size): |
|---|
| 82 | s = s[:size] |
|---|
| 83 | return (' '*(size-len(s)))+s |
|---|
| 84 | |
|---|
| 85 | |
|---|
| 86 | class CursesDisplayer(object): |
|---|
| 87 | |
|---|
| 88 | def __init__(self, scrwin): |
|---|
| 89 | self.messages = [] |
|---|
| 90 | self.scroll_pos = 0 |
|---|
| 91 | self.scroll_time = 0 |
|---|
| 92 | |
|---|
| 93 | self.scrwin = scrwin |
|---|
| 94 | signal(SIGWINCH, self.winch_handler) |
|---|
| 95 | self.changeflag = Event() |
|---|
| 96 | self._remake_window() |
|---|
| 97 | |
|---|
| 98 | def winch_handler(self, signum, stackframe): |
|---|
| 99 | self.changeflag.set() |
|---|
| 100 | curses.endwin() |
|---|
| 101 | self.scrwin.refresh() |
|---|
| 102 | self.scrwin = curses.newwin(0, 0, 0, 0) |
|---|
| 103 | self._remake_window() |
|---|
| 104 | self._display_messages() |
|---|
| 105 | |
|---|
| 106 | def _remake_window(self): |
|---|
| 107 | self.scrh, self.scrw = self.scrwin.getmaxyx() |
|---|
| 108 | self.scrpan = curses.panel.new_panel(self.scrwin) |
|---|
| 109 | self.mainwinh = (2*self.scrh)//3 |
|---|
| 110 | self.mainwinw = self.scrw - 4 # - 2 (bars) - 2 (spaces) |
|---|
| 111 | self.mainwiny = 2 # + 1 (bar) + 1 (titles) |
|---|
| 112 | self.mainwinx = 2 # + 1 (bar) + 1 (space) |
|---|
| 113 | # + 1 to all windows so we can write at mainwinw |
|---|
| 114 | |
|---|
| 115 | self.mainwin = curses.newwin(self.mainwinh, self.mainwinw+1, |
|---|
| 116 | self.mainwiny, self.mainwinx) |
|---|
| 117 | self.mainpan = curses.panel.new_panel(self.mainwin) |
|---|
| 118 | self.mainwin.scrollok(0) |
|---|
| 119 | self.mainwin.nodelay(1) |
|---|
| 120 | |
|---|
| 121 | self.headerwin = curses.newwin(1, self.mainwinw+1, |
|---|
| 122 | 1, self.mainwinx) |
|---|
| 123 | self.headerpan = curses.panel.new_panel(self.headerwin) |
|---|
| 124 | self.headerwin.scrollok(0) |
|---|
| 125 | |
|---|
| 126 | self.totalwin = curses.newwin(1, self.mainwinw+1, |
|---|
| 127 | self.mainwinh+1, self.mainwinx) |
|---|
| 128 | self.totalpan = curses.panel.new_panel(self.totalwin) |
|---|
| 129 | self.totalwin.scrollok(0) |
|---|
| 130 | |
|---|
| 131 | self.statuswinh = self.scrh-4-self.mainwinh |
|---|
| 132 | self.statuswin = curses.newwin(self.statuswinh, self.mainwinw+1, |
|---|
| 133 | self.mainwinh+3, self.mainwinx) |
|---|
| 134 | self.statuspan = curses.panel.new_panel(self.statuswin) |
|---|
| 135 | self.statuswin.scrollok(0) |
|---|
| 136 | |
|---|
| 137 | try: |
|---|
| 138 | self.scrwin.border(ord('|'),ord('|'),ord('-'),ord('-'),ord(' '),ord(' '),ord(' '),ord(' ')) |
|---|
| 139 | except: |
|---|
| 140 | pass |
|---|
| 141 | self.headerwin.addnstr(0, 2, '#', self.mainwinw - 25, curses.A_BOLD) |
|---|
| 142 | self.headerwin.addnstr(0, 4, 'Filename', self.mainwinw - 25, curses.A_BOLD) |
|---|
| 143 | self.headerwin.addnstr(0, self.mainwinw - 24, 'Size', 4, curses.A_BOLD) |
|---|
| 144 | self.headerwin.addnstr(0, self.mainwinw - 18, 'Download', 8, curses.A_BOLD) |
|---|
| 145 | self.headerwin.addnstr(0, self.mainwinw - 6, 'Upload', 6, curses.A_BOLD) |
|---|
| 146 | self.totalwin.addnstr(0, self.mainwinw - 27, 'Totals:', 7, curses.A_BOLD) |
|---|
| 147 | |
|---|
| 148 | self._display_messages() |
|---|
| 149 | |
|---|
| 150 | curses.panel.update_panels() |
|---|
| 151 | curses.doupdate() |
|---|
| 152 | self.changeflag.clear() |
|---|
| 153 | |
|---|
| 154 | |
|---|
| 155 | def _display_line(self, s, bold = False): |
|---|
| 156 | if self.disp_end: |
|---|
| 157 | return True |
|---|
| 158 | line = self.disp_line |
|---|
| 159 | self.disp_line += 1 |
|---|
| 160 | if line < 0: |
|---|
| 161 | return False |
|---|
| 162 | if bold: |
|---|
| 163 | self.mainwin.addnstr(line, 0, s, self.mainwinw, curses.A_BOLD) |
|---|
| 164 | else: |
|---|
| 165 | self.mainwin.addnstr(line, 0, s, self.mainwinw) |
|---|
| 166 | if self.disp_line >= self.mainwinh: |
|---|
| 167 | self.disp_end = True |
|---|
| 168 | return self.disp_end |
|---|
| 169 | |
|---|
| 170 | def _display_data(self, data): |
|---|
| 171 | if 3*len(data) <= self.mainwinh: |
|---|
| 172 | self.scroll_pos = 0 |
|---|
| 173 | self.scrolling = False |
|---|
| 174 | elif self.scroll_time + DOWNLOAD_SCROLL_RATE < time(): |
|---|
| 175 | self.scroll_time = time() |
|---|
| 176 | self.scroll_pos += 1 |
|---|
| 177 | self.scrolling = True |
|---|
| 178 | if self.scroll_pos >= 3*len(data)+2: |
|---|
| 179 | self.scroll_pos = 0 |
|---|
| 180 | |
|---|
| 181 | i = self.scroll_pos//3 |
|---|
| 182 | self.disp_line = (3*i)-self.scroll_pos |
|---|
| 183 | self.disp_end = False |
|---|
| 184 | |
|---|
| 185 | while not self.disp_end: |
|---|
| 186 | ii = i % len(data) |
|---|
| 187 | if i and not ii: |
|---|
| 188 | if not self.scrolling: |
|---|
| 189 | break |
|---|
| 190 | self._display_line('') |
|---|
| 191 | if self._display_line(''): |
|---|
| 192 | break |
|---|
| 193 | ( name, status, progress, peers, seeds, seedsmsg, dist, |
|---|
| 194 | uprate, dnrate, upamt, dnamt, size, t, msg ) = data[ii] |
|---|
| 195 | t = fmttime(t) |
|---|
| 196 | if t: |
|---|
| 197 | status = t |
|---|
| 198 | name = ljust(name,self.mainwinw-32) |
|---|
| 199 | size = rjust(fmtsize(size),8) |
|---|
| 200 | uprate = rjust('%s/s' % fmtsize(uprate),10) |
|---|
| 201 | dnrate = rjust('%s/s' % fmtsize(dnrate),10) |
|---|
| 202 | line = "%3d %s%s%s%s" % (ii+1, name, size, dnrate, uprate) |
|---|
| 203 | self._display_line(line, True) |
|---|
| 204 | if peers + seeds: |
|---|
| 205 | datastr = ' (%s) %s - %s peers %s seeds %s dist copies - %s up %s dn' % ( |
|---|
| 206 | progress, status, peers, seeds, dist, |
|---|
| 207 | fmtsize(upamt), fmtsize(dnamt) ) |
|---|
| 208 | else: |
|---|
| 209 | datastr = ' '+status+' ('+progress+')' |
|---|
| 210 | self._display_line(datastr) |
|---|
| 211 | self._display_line(' '+ljust(msg,self.mainwinw-4)) |
|---|
| 212 | i += 1 |
|---|
| 213 | |
|---|
| 214 | def display(self, data): |
|---|
| 215 | if self.changeflag.isSet(): |
|---|
| 216 | return |
|---|
| 217 | |
|---|
| 218 | inchar = self.mainwin.getch() |
|---|
| 219 | if inchar == 12: # ^L |
|---|
| 220 | self._remake_window() |
|---|
| 221 | |
|---|
| 222 | self.mainwin.erase() |
|---|
| 223 | if data: |
|---|
| 224 | self._display_data(data) |
|---|
| 225 | else: |
|---|
| 226 | self.mainwin.addnstr( 1, self.mainwinw//2-5, |
|---|
| 227 | 'no torrents', 12, curses.A_BOLD ) |
|---|
| 228 | totalup = 0 |
|---|
| 229 | totaldn = 0 |
|---|
| 230 | for ( name, status, progress, peers, seeds, seedsmsg, dist, |
|---|
| 231 | uprate, dnrate, upamt, dnamt, size, t, msg ) in data: |
|---|
| 232 | totalup += uprate |
|---|
| 233 | totaldn += dnrate |
|---|
| 234 | |
|---|
| 235 | totalup = '%s/s' % fmtsize(totalup) |
|---|
| 236 | totaldn = '%s/s' % fmtsize(totaldn) |
|---|
| 237 | |
|---|
| 238 | self.totalwin.erase() |
|---|
| 239 | self.totalwin.addnstr(0, self.mainwinw-27, 'Totals:', 7, curses.A_BOLD) |
|---|
| 240 | self.totalwin.addnstr(0, self.mainwinw-20 + (10-len(totaldn)), |
|---|
| 241 | totaldn, 10, curses.A_BOLD) |
|---|
| 242 | self.totalwin.addnstr(0, self.mainwinw-10 + (10-len(totalup)), |
|---|
| 243 | totalup, 10, curses.A_BOLD) |
|---|
| 244 | |
|---|
| 245 | curses.panel.update_panels() |
|---|
| 246 | curses.doupdate() |
|---|
| 247 | |
|---|
| 248 | return inchar in (ord('q'),ord('Q')) |
|---|
| 249 | |
|---|
| 250 | def message(self, s): |
|---|
| 251 | self.messages.append(strftime('%x %X - ',localtime(time()))+s) |
|---|
| 252 | self._display_messages() |
|---|
| 253 | |
|---|
| 254 | def _display_messages(self): |
|---|
| 255 | self.statuswin.erase() |
|---|
| 256 | winpos = 0 |
|---|
| 257 | for s in self.messages[-self.statuswinh:]: |
|---|
| 258 | self.statuswin.addnstr(winpos, 0, s, self.mainwinw) |
|---|
| 259 | winpos += 1 |
|---|
| 260 | curses.panel.update_panels() |
|---|
| 261 | curses.doupdate() |
|---|
| 262 | |
|---|
| 263 | def exception(self, s): |
|---|
| 264 | exceptions.append(s) |
|---|
| 265 | self.message('SYSTEM ERROR - EXCEPTION GENERATED') |
|---|
| 266 | |
|---|
| 267 | |
|---|
| 268 | |
|---|
| 269 | def LaunchManyWrapper(scrwin, config): |
|---|
| 270 | LaunchMany(config, CursesDisplayer(scrwin), 'anonlaunchmanycurses') |
|---|
| 271 | |
|---|
| 272 | |
|---|
| 273 | if __name__ == '__main__': |
|---|
| 274 | import Anomos |
|---|
| 275 | Anomos.ensure_minimum_config() |
|---|
| 276 | Anomos.init_logging() |
|---|
| 277 | |
|---|
| 278 | uiname = 'anonlaunchmanycurses' |
|---|
| 279 | defaults = get_defaults(uiname) |
|---|
| 280 | try: |
|---|
| 281 | if len(sys.argv) < 2: |
|---|
| 282 | printHelp(uiname, defaults) |
|---|
| 283 | sys.exit(1) |
|---|
| 284 | config, args = configfile.parse_configuration_and_args(defaults, |
|---|
| 285 | uiname, sys.argv[1:], 0, 1) |
|---|
| 286 | if args: |
|---|
| 287 | config['torrent_dir'] = args[0] |
|---|
| 288 | if not os.path.isdir(config['torrent_dir']): |
|---|
| 289 | raise BTFailure("Warning: "+args[0]+" is not a directory") |
|---|
| 290 | except BTFailure, e: |
|---|
| 291 | print 'error: ' + str(e) + '\nrun with no args for parameter explanations' |
|---|
| 292 | sys.exit(1) |
|---|
| 293 | |
|---|
| 294 | curses_wrapper(LaunchManyWrapper, config) |
|---|
| 295 | if exceptions: |
|---|
| 296 | print '\nEXCEPTION:' |
|---|
| 297 | print exceptions[0] |
|---|