diff --git a/deluge/ui/console/modes/addtorrents.py b/deluge/ui/console/modes/addtorrents.py new file mode 100644 index 000000000..c6bc4fae7 --- /dev/null +++ b/deluge/ui/console/modes/addtorrents.py @@ -0,0 +1,534 @@ +# -*- coding: utf-8 -*- +# +# addtorrents.py +# +# Copyright (C) 2012 Arek StefaƄski +# +# Deluge is free software. +# +# You may redistribute it and/or modify it under the terms of the +# GNU General Public License, as published by the Free Software +# Foundation; either version 3 of the License, or (at your option) +# any later version. +# +# deluge is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with deluge. If not, write to: +# The Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor +# Boston, MA 02110-1301, USA. +# +# In addition, as a special exception, the copyright holders give +# permission to link the code of portions of this program with the OpenSSL +# library. +# You must obey the GNU General Public License in all respects for all of +# the code used other than OpenSSL. If you modify file(s) with this +# exception, you may extend this exception to your version of the file(s), +# but you are not obligated to do so. If you do not wish to do so, delete +# this exception statement from your version. If you delete this exception +# statement from all source files in the program, then also delete it here. +# +# + +import deluge.component as component +from basemode import BaseMode + +import deluge.common as common +from deluge.ui.client import client + +import os +import base64 + +from deluge.ui.sessionproxy import SessionProxy + +from input_popup import InputPopup +import deluge.ui.console.colors as colors +import format_utils + +try: + import curses +except ImportError: + pass + +import logging +log = logging.getLogger(__name__) + +# Big help string that gets displayed when the user hits 'h' +HELP_STR = """\ +To be written +""" + +class AddTorrents(BaseMode, component.Component): + def __init__(self, alltorrentmode, stdscr, console_config, encoding=None): + + self.console_config = console_config + + self.alltorrentmode = alltorrentmode + + self.popup = None + + self.view_offset = 0 + self.cursel = 0 + self.marked = set() + self.last_mark = -1 + + self.path_stack = ["/"] + "/home/asmageddon/Downloads/torrents/".split("/")[1:-1] + self.path_stack_pos = len(self.path_stack) + self.listing_files = [] + self.listing_dirs = [] + + self.raw_rows = [] + self.raw_rows_files = [] + self.raw_rows_dirs = [] + self.formatted_rows = [] + + self.sort_column = 2 + self.reverse_sort = True + + BaseMode.__init__(self, stdscr, encoding) + + self._listing_space = self.rows - 5 + + self.__refresh_listing() + + + component.Component.__init__(self, "AddTorrents", 1, depend=["SessionProxy"]) + + self.__split_help() + + component.start(["AddTorrents"]) + + curses.curs_set(0) + self.stdscr.notimeout(0) + + # component start/update + def start(self): + pass + def update(self): + pass + + def __refresh_listing(self): + self.listing_files = [] + self.listing_dirs = [] + + self.raw_rows = [] + self.raw_rows_files = [] + self.raw_rows_dirs = [] + self.formatted_rows = [] + + path = os.path.join(*self.path_stack[:self.path_stack_pos]) + + listing = os.listdir(path) + for f in listing: + if os.path.isdir(os.path.join(path, f)): + if self.console_config["addtorrents_show_hidden_folders"]: + self.listing_dirs.append(f) + elif f[0] != ".": + self.listing_dirs.append(f) + elif os.path.isfile(os.path.join(path, f)): + if self.console_config["addtorrents_show_misc_files"]: + self.listing_files.append(f) + elif f.endswith(".torrent"): + self.listing_files.append(f) + + for dirname in self.listing_dirs: + row = [] + full_path = os.path.join(path, dirname) + try: + size = len(os.listdir(full_path)) + except: + size = 0 + time = os.stat(full_path).st_mtime + + row = [dirname, size, time, full_path, 1] + + self.raw_rows.append( row ) + self.raw_rows_dirs.append( row ) + + #Highlight the directory we came from + if self.path_stack_pos < len(self.path_stack): + selected = self.path_stack[self.path_stack_pos] + ld = sorted(self.listing_dirs, key=lambda n: n.lower()) + c = ld.index(selected) + self.cursel = c + + if (self.view_offset + self._listing_space) <= self.cursel: + self.view_offset = self.cursel - self._listing_space + + for filename in self.listing_files: + row = [] + full_path = os.path.join(path, filename) + size = os.stat(full_path).st_size + time = os.stat(full_path).st_mtime + + row = [filename, size, time, full_path, 0] + + self.raw_rows.append( row ) + self.raw_rows_files.append( row ) + + self.__sort_rows() + + def __sort_rows(self): + + self.raw_rows_dirs.sort(key=lambda r: r[0].lower()) + + if self.sort_column == 0: + self.raw_rows_files.sort(key=lambda r: r[0].lower(), reverse=self.reverse_sort) + else: + self.raw_rows_files.sort(key=lambda r: r[2], reverse=self.reverse_sort) + self.raw_rows = self.raw_rows_dirs + self.raw_rows_files + + self.__refresh_rows() + + def __refresh_rows(self): + self.formatted_rows = [] + + for row in self.raw_rows: + filename = row[0] + size = row[1] + time = row[2] + + if row[4]: + cols = [filename.decode("utf8"), "%i items" % size, common.fdate(time)] + widths = [self.cols - 35, 12, 23] + self.formatted_rows.append(format_utils.format_row(cols, widths)) + else: + #Size of .torrent file itself couldn't matter less so we'll leave it out + cols = [filename.decode("utf8"), common.fdate(time)] + widths = [self.cols - 23, 23] + self.formatted_rows.append(format_utils.format_row(cols, widths)) + + + def __split_help(self): + self.__help_lines = format_utils.wrap_string(HELP_STR,(self.cols/2)-2) + + def scroll_list_up(self, distance): + self.cursel -= distance + if self.cursel < 0: + self.cursel = 0 + + if self.cursel < self.view_offset + 1: + self.view_offset = max(self.cursel - 1, 0) + + self.path_stack = self.path_stack[:self.path_stack_pos] + + def scroll_list_down(self, distance): + self.cursel += distance + if self.cursel >= len(self.formatted_rows): + self.cursel = len(self.formatted_rows) - 1 + + if (self.view_offset + self._listing_space) <= self.cursel + 1: + self.view_offset = self.cursel - self._listing_space + 1 + + self.path_stack = self.path_stack[:self.path_stack_pos] + + def set_popup(self,pu): + self.popup = pu + self.refresh() + + def on_resize(self, *args): + BaseMode.on_resize_norefresh(self, *args) + + #Always refresh Legacy(it will also refresh AllTorrents), otherwise it will bug deluge out + legacy = component.get("LegacyUI") + legacy.on_resize(*args) + + self.__split_help() + if self.popup: + self.popup.handle_resize() + + self._listing_space = self.rows - 5 + + self.refresh() + + def refresh(self,lines=None): + + # Update the status bars + self.stdscr.erase() + self.add_string(0, self.statusbars.topbar) + + #This will quite likely fail when switching modes + try: + rf = format_utils.remove_formatting + string = self.statusbars.bottombar + hstr = "Press {!magenta,blue,bold!}[h]{!status!} for help" + + string += " " * ( self.cols - len(rf(string)) - len(rf(hstr))) + hstr + + self.add_string(self.rows - 1, string) + except: + pass + + off = 1 + + #Render breadcrumbs + s = "Location: " + for i, e in enumerate(self.path_stack): + if e == "/": + if i == self.path_stack_pos - 1: + s += "{!black,red,bold!}root" + else: + s += "{!red,black,bold!}root" + else: + if i == self.path_stack_pos - 1: + s += "{!black,white,bold!}%s" % e + else: + s += "{!white,black,bold!}%s" % e + + if e != len(self.path_stack) - 1: + s += "{!white,black!}/" + + self.add_string(off, s) + off += 1 + + #Render header + cols = ["Name", "Contents", "Modification time"] + widths = [self.cols - 35, 12, 23] + s = "" + for i, (c, w) in enumerate(zip(cols, widths)): + if i == self.sort_column: + s += "{!black,green,bold!}" + c.ljust(w - 2) + if self.reverse_sort: + s += "^ " + else: + s += "v " + else: + s += "{!green,black,bold!}" + c.ljust(w) + self.add_string(off, s) + off += 1 + + #Render files and folders + for i, row in enumerate(self.formatted_rows[self.view_offset:]): + i += self.view_offset + #It's a folder + color_string = "" + if self.raw_rows[i][4]: + if i == self.cursel: + color_string = "{!black,cyan,bold!}" + else: + color_string = "{!cyan,black!}" + elif i == self.cursel: + if self.raw_rows[i][0] in self.marked: + color_string = "{!blue,white,bold!}" + else: + color_string = "{!black,white,bold!}" + elif self.raw_rows[i][0] in self.marked: + color_string = "{!white,blue,bold!}" + + self.add_string(off, color_string + row) + off+= 1 + + if off > self.rows - 2: + break + + if component.get("ConsoleUI").screen != self: + return + + self.stdscr.noutrefresh() + + if self.popup: + self.popup.refresh() + + curses.doupdate() + + def back_to_overview(self): + component.stop(["AddTorrents"]) + component.deregister(self) + self.stdscr.erase() + component.get("ConsoleUI").set_mode(self.alltorrentmode) + self.alltorrentmode._go_top = False + self.alltorrentmode.resume() + + def _perform_action(self): + if self.cursel < len(self.listing_dirs): + self._enter_dir() + else: + s = self.raw_rows[self.cursel][0] + self.marked.add(s) + self._show_add_dialog() + + def _enter_dir(self): + #Enter currently selected directory + dirname = self.raw_rows[self.cursel][0] + new_dir = self.path_stack_pos >= len(self.path_stack) + new_dir = new_dir or (dirname != self.path_stack[self.path_stack_pos]) + if new_dir: + self.path_stack = self.path_stack[:self.path_stack_pos] + self.path_stack.append(dirname) + + self.path_stack_pos += 1 + + self.view_offset = 0 + self.cursel = 0 + self.last_mark = -1 + self.marked = [] + + self.__refresh_listing() + + def _show_add_dialog(self): + + def _do_add(result): + ress = {"succ":0, + "fail":0, + "total": len(self.marked), + "fmsg":[]} + + def fail_cb(msg,t_file,ress): + log.debug("failed to add torrent: %s: %s"%(t_file,msg)) + ress["fail"]+=1 + ress["fmsg"].append("%s: %s"%(t_file,msg)) + if (ress["succ"]+ress["fail"]) >= ress["total"]: + self.alltorrentmode._report_add_status(ress["succ"],ress["fail"],ress["fmsg"]) + + def success_cb(tid,t_file,ress): + if tid: + log.debug("added torrent: %s (%s)"%(t_file,tid)) + ress["succ"]+=1 + if (ress["succ"]+ress["fail"]) >= ress["total"]: + self.alltorrentmode._report_add_status(ress["succ"],ress["fail"],ress["fmsg"]) + else: + fail_cb("Already in session (probably)",t_file,ress) + + for m in self.marked: + filename = m + directory = os.path.join(*self.path_stack[:self.path_stack_pos]) + path = os.path.join(directory, filename) + filedump = base64.encodestring(open(path).read()) + t_options = {} + if result["location"]: + t_options["download_location"] = result["location"] + t_options["add_paused"] = result["add_paused"] + + d = client.core.add_torrent_file(filename, filedump, t_options) + d.addCallback(success_cb, path, ress) + d.addErrback(fail_cb, path, ress) + + self.back_to_overview() + + config = component.get("ConsoleUI").coreconfig + dl = config["download_location"] + if config["add_paused"]: + ap = 0 + else: + ap = 1 + self.popup = InputPopup(self,"Add Torrents (Esc to cancel)",close_cb=_do_add, height_req=17) + + msg = "Adding torrent files:" + for i, m in enumerate(self.marked): + name = m + msg += "\n * {!input!}%s" % name + if i == 5: + if i < len(self.marked): + msg += "\n {!red!}And %i more" % (len(self.marked) - 5) + break + self.popup.add_text(msg) + self.popup.add_spaces(1) + + self.popup.add_text_input("Save Location:","location", dl) + self.popup.add_select_input("Add Paused:","add_paused",["Yes","No"],[True,False],ap) + + + def _go_up(self): + #Go up in directory hierarchy + if self.path_stack_pos > 1: + self.path_stack_pos -= 1 + + self.view_offset = 0 + self.cursel = 0 + self.last_mark = -1 + self.marked = set() + + self.__refresh_listing() + + def _doRead(self): + c = self.stdscr.getch() + + if self.popup: + if self.popup.handle_read(c): + self.popup = None + self.refresh() + return + + if c > 31 and c < 256: + if chr(c) == 'Q': + from twisted.internet import reactor + if client.connected(): + def on_disconnect(result): + reactor.stop() + client.disconnect().addCallback(on_disconnect) + else: + reactor.stop() + return + elif chr(c) == 'q': + self.back_to_overview() + return + + # Navigate the torrent list + if c == curses.KEY_UP: + self.scroll_list_up(1) + elif c == curses.KEY_PPAGE: + #self.scroll_list_up(self._listing_space-2) + self.scroll_list_up(self.rows // 2) + elif c == curses.KEY_HOME: + self.scroll_list_up(len(self.formatted_rows)) + elif c == curses.KEY_DOWN: + self.scroll_list_down(1) + elif c == curses.KEY_NPAGE: + #self.scroll_list_down(self._listing_space-2) + self.scroll_list_down(self.rows // 2) + elif c == curses.KEY_END: + self.scroll_list_down(len(self.formatted_rows)) + elif c == curses.KEY_RIGHT: + self._perform_action() + elif c == curses.KEY_LEFT: + self._go_up() + # Enter Key + elif c == curses.KEY_ENTER or c == 10: + self._perform_action() + # space + elif c == 32: + self._perform_action() + else: + if c > 31 and c < 256: + if chr(c) == 'h': + self.popup = Popup(self,"Help",init_lines=self.__help_lines, height_req=0.75, width_req=65) + elif chr(c) == '>': + if self.sort_column == 2: + self.reverse_sort = not self.reverse_sort + else: + self.sort_column = 2 + self.reverse_sort = True + self.__sort_rows() + elif chr(c) == '<': + if self.sort_column == 0: + self.reverse_sort = not self.reverse_sort + else: + self.sort_column = 0 + self.reverse_sort = False + self.__sort_rows() + elif chr(c) == 'm': + s = self.raw_rows[self.cursel][0] + if s in self.marked: + self.marked.remove(s) + else: + self.marked.add(s) + + self.last_mark = self.cursel + elif chr(c) == 'M': + if self.last_mark != -1: + if self.last_mark > self.cursel: + m = range(self.cursel, self.last_mark) + else: + m = range(self.last_mark, self.cursel + 1) + + for i in m: + s = self.raw_rows[i][0] + self.marked.add(s) + elif chr(c) == 'c': + self.marked.clear() + + self.refresh() diff --git a/deluge/ui/console/modes/alltorrents.py b/deluge/ui/console/modes/alltorrents.py index 2e9c09138..76bd0c560 100644 --- a/deluge/ui/console/modes/alltorrents.py +++ b/deluge/ui/console/modes/alltorrents.py @@ -48,6 +48,7 @@ from popup import Popup, SelectablePopup, MessagePopup, ALIGN from add_util import add_torrent from input_popup import InputPopup, ALIGN from torrentdetail import TorrentDetail +from addtorrents import AddTorrents from preferences import Preferences from torrent_actions import torrent_actions_popup, ACTION from eventview import EventView @@ -189,7 +190,9 @@ DEFAULT_PREFS = { "separate_complete": True, "ring_bell": False, "save_legacy_history": True, - "first_run": True + "first_run": True, + "addtorrents_show_misc_files": False, + "addtorrents_show_hidden_folders": False } column_pref_names = ["queue","name","size","state", @@ -575,6 +578,14 @@ class AllTorrents(BaseMode, component.Component): else: return "" + def show_addtorrents_screen(self): + def dodeets(arg): + if arg and True in arg[0]: + self.stdscr.erase() + component.get("ConsoleUI").set_mode(AddTorrents(self,self.stdscr, self.config, self.encoding)) + else: + self.messages.append(("Error","An error occured trying to display add torrents screen")) + component.stop(["AllTorrents"]).addCallback(dodeets) def show_torrent_details(self,tid): def dodeets(arg): @@ -663,11 +674,11 @@ class AllTorrents(BaseMode, component.Component): self.popup.add_line("_Checking",data=FILTER.CHECKING,foreground="blue") self.popup.add_line("Q_ueued",data=FILTER.QUEUED,foreground="yellow") - def __report_add_status(self, succ_cnt, fail_cnt, fail_msgs): + def _report_add_status(self, succ_cnt, fail_cnt, fail_msgs): if fail_cnt == 0: self.report_message("Torrents Added","{!success!}Successfully added %d torrent(s)"%succ_cnt) else: - msg = ("{!error!}Failed to add the following %d torrent(s):\n {!error!}"%fail_cnt)+"\n {!error!}".join(fail_msgs) + msg = ("{!error!}Failed to add the following %d torrent(s):\n {!input!}"%fail_cnt)+"\n {!error!}".join(fail_msgs) if succ_cnt != 0: msg += "\n \n{!success!}Successfully added %d torrent(s)"%succ_cnt self.report_message("Torrent Add Report",msg) @@ -685,13 +696,13 @@ class AllTorrents(BaseMode, component.Component): ress["fail"]+=1 ress["fmsg"].append("%s: %s"%(t_file,msg)) if (ress["succ"]+ress["fail"]) >= ress["total"]: - self.__report_add_status(ress["succ"],ress["fail"],ress["fmsg"]) + self._report_add_status(ress["succ"],ress["fail"],ress["fmsg"]) def suc_cb(tid,t_file,ress): if tid: log.debug("added torrent: %s (%s)"%(t_file,tid)) ress["succ"]+=1 if (ress["succ"]+ress["fail"]) >= ress["total"]: - self.__report_add_status(ress["succ"],ress["fail"],ress["fmsg"]) + self._report_add_status(ress["succ"],ress["fail"],ress["fmsg"]) else: fail_cb("Already in session (probably)",t_file,ress) @@ -1187,6 +1198,8 @@ class AllTorrents(BaseMode, component.Component): self.last_mark = -1 elif chr(c) == 'a': self._show_torrent_add_popup() + elif chr(c) == 'A': + self.show_addtorrents_screen() elif chr(c) == 'v': self._show_visible_columns_popup()