PageRenderTime 27ms CodeModel.GetById 18ms RepoModel.GetById 1ms app.codeStats 0ms

/btlaunchmanycurses.py

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