class AllTorrents(BaseMode, component.Component): def __init__(self, stdscr, encoding=None): self.formatted_rows = None self.torrent_names = None self.cursel = 1 self.curoff = 1 # TODO: this should really be 0 indexed self.column_string = "" self.popup = None self.messages = deque() self.marked = [] self.last_mark = -1 self._sorted_ids = None self._go_top = False self._curr_filter = None self.entering_search = False self.search_string = None self.cursor = 0 self.coreconfig = component.get("ConsoleUI").coreconfig self.legacy_mode = None self.__status_dict = {} self.__torrent_info_id = None BaseMode.__init__(self, stdscr, encoding) component.Component.__init__(self, "AllTorrents", 1, depend=["SessionProxy"]) curses.curs_set(0) self.stdscr.notimeout(0) self.__split_help() self.update_config() component.start(["AllTorrents"]) self._info_fields = [ ("Name",None,("name",)), ("State", None, ("state",)), ("Down Speed", format_utils.format_speed, ("download_payload_rate",)), ("Up Speed", format_utils.format_speed, ("upload_payload_rate",)), ("Progress", format_utils.format_progress, ("progress",)), ("ETA", deluge.common.ftime, ("eta",)), ("Path", None, ("save_path",)), ("Downloaded",deluge.common.fsize,("all_time_download",)), ("Uploaded", deluge.common.fsize,("total_uploaded",)), ("Share Ratio", format_utils.format_float, ("ratio",)), ("Seeders",format_utils.format_seeds_peers,("num_seeds","total_seeds")), ("Peers",format_utils.format_seeds_peers,("num_peers","total_peers")), ("Active Time",deluge.common.ftime,("active_time",)), ("Seeding Time",deluge.common.ftime,("seeding_time",)), ("Date Added",deluge.common.fdate,("time_added",)), ("Availability", format_utils.format_float, ("distributed_copies",)), ("Pieces", format_utils.format_pieces, ("num_pieces","piece_length")), ] self.__status_keys = ["name","state","download_payload_rate","upload_payload_rate", "progress","eta","all_time_download","total_uploaded", "ratio", "num_seeds","total_seeds","num_peers","total_peers", "active_time", "seeding_time","time_added","distributed_copies", "num_pieces", "piece_length","save_path"] # component start/update def start(self): component.get("SessionProxy").get_torrents_status(self.__status_dict, self.__status_fields).addCallback(self.set_state,False) def update(self): component.get("SessionProxy").get_torrents_status(self.__status_dict, self.__status_fields).addCallback(self.set_state,True) if self.__torrent_info_id: component.get("SessionProxy").get_torrent_status(self.__torrent_info_id, self.__status_keys).addCallback(self._on_torrent_status) def update_config(self): self.config = ConfigManager("console.conf",DEFAULT_PREFS) self.__cols_to_show = [pref for pref in column_pref_names if self.config["show_%s"%pref]] self.__columns = [prefs_to_names[col] for col in self.__cols_to_show] self.__status_fields = column.get_required_fields(self.__columns) for rf in ["state","name","queue"]: # we always need these, even if we're not displaying them if not rf in self.__status_fields: self.__status_fields.append(rf) self.__update_columns() def __split_help(self): self.__help_lines = format_utils.wrap_string(HELP_STR,(self.cols/2)-2) def resume(self): self._go_top = True component.start(["AllTorrents"]) self.refresh() def __update_columns(self): self.column_widths = [self.config["%s_width"%c] for c in self.__cols_to_show] req = sum(filter(lambda x:x >= 0,self.column_widths)) if (req > self.cols): # can't satisfy requests, just spread out evenly cw = int(self.cols/len(self.__columns)) for i in range(0,len(self.column_widths)): self.column_widths[i] = cw else: rem = self.cols - req var_cols = len(filter(lambda x: x < 0,self.column_widths)) if (var_cols > 0): vw = int(rem/var_cols) for i in range(0, len(self.column_widths)): if (self.column_widths[i] < 0): self.column_widths[i] = vw self.column_string = "{!header!}%s"%("".join(["%s%s"%(self.__columns[i]," "*(self.column_widths[i]-len(self.__columns[i]))) for i in range(0,len(self.__columns))])) def set_state(self, state, refresh): self.curstate = state # cache in case we change sort order newnames = [] newrows = [] self._sorted_ids = self._sort_torrents(self.curstate) for torrent_id in self._sorted_ids: ts = self.curstate[torrent_id] newnames.append(ts["name"]) newrows.append((format_utils.format_row([column.get_column_value(name,ts) for name in self.__columns],self.column_widths),ts["state"])) self.numtorrents = len(state) self.formatted_rows = newrows self.torrent_names = newnames if refresh: self.refresh() def get_torrent_name(self, torrent_id): for p,i in enumerate(self._sorted_ids): if torrent_id == i: return self.torrent_names[p] return None def _scroll_up(self, by): prevoff = self.curoff self.cursel = max(self.cursel - by,1) if ((self.cursel - 1) < self.curoff): self.curoff = max(self.cursel - 1,1) return prevoff != self.curoff def _scroll_down(self, by): prevoff = self.curoff self.cursel = min(self.cursel + by,self.numtorrents) if ((self.curoff + self.rows - 5) < self.cursel): self.curoff = self.cursel - self.rows + 5 return prevoff != self.curoff def current_torrent_id(self): if self._sorted_ids: return self._sorted_ids[self.cursel-1] else: return None def _selected_torrent_ids(self): ret = [] for i in self.marked: ret.append(self._sorted_ids[i-1]) return ret def _on_torrent_status(self, state): if (self.popup): self.popup.clear() name = state["name"] off = int((self.cols/4)-(len(name)/2)) self.popup.set_title(name) for i,f in enumerate(self._info_fields): if f[1] != None: args = [] try: for key in f[2]: args.append(state[key]) except: log.debug("Could not get info field: %s",e) continue info = f[1](*args) else: info = state[f[2][0]] nl = len(f[0])+4 if (nl+len(info))>self.popup.width: self.popup.add_line("{!info!}%s: {!input!}%s"%(f[0],info[:(self.popup.width - nl)])) info = info[(self.popup.width - nl):] n = self.popup.width-3 chunks = [info[i:i+n] for i in xrange(0, len(info), n)] for c in chunks: self.popup.add_line(" %s"%c) else: self.popup.add_line("{!info!}%s: {!input!}%s"%(f[0],info)) self.refresh() else: self.__torrent_info_id = None def on_resize(self, *args): BaseMode.on_resize_norefresh(self, *args) self.__update_columns() self.__split_help() if self.popup: self.popup.handle_resize() self.refresh() def _queue_sort(self, v1, v2): if v1 == v2: return 0 if v2 < 0: return -1 if v1 < 0: return 1 if v1 > v2: return 1 if v2 > v1: return -1 def _sort_torrents(self, state): "sorts by queue #" return sorted(state,cmp=self._queue_sort,key=lambda s:state.get(s)["queue"]) def _format_queue(self, qnum): if (qnum >= 0): return "%d"%(qnum+1) else: return "" def show_torrent_details(self,tid): def dodeets(arg): if arg and True in arg[0]: self.stdscr.clear() component.get("ConsoleUI").set_mode(TorrentDetail(self,tid,self.stdscr,self.encoding)) else: self.messages.append(("Error","An error occured trying to display torrent details")) component.stop(["AllTorrents"]).addCallback(dodeets) def show_preferences(self): def _on_get_config(config): client.core.get_listen_port().addCallback(_on_get_listen_port,config) def _on_get_listen_port(port,config): client.core.get_cache_status().addCallback(_on_get_cache_status,port,config) def _on_get_cache_status(status,port,config): def doprefs(arg): if arg and True in arg[0]: self.stdscr.clear() component.get("ConsoleUI").set_mode(Preferences(self,config,self.config,port,status,self.stdscr,self.encoding)) else: self.messages.append(("Error","An error occured trying to display preferences")) component.stop(["AllTorrents"]).addCallback(doprefs) client.core.get_config().addCallback(_on_get_config) def __show_events(self): def doevents(arg): if arg and True in arg[0]: self.stdscr.clear() component.get("ConsoleUI").set_mode(EventView(self,self.stdscr,self.encoding)) else: self.messages.append(("Error","An error occured trying to display events")) component.stop(["AllTorrents"]).addCallback(doevents) def __legacy_mode(self): def dolegacy(arg): if arg and True in arg[0]: self.stdscr.clear() if not self.legacy_mode: self.legacy_mode = Legacy(self.stdscr,self.encoding) component.get("ConsoleUI").set_mode(self.legacy_mode) self.legacy_mode.refresh() curses.curs_set(2) else: self.messages.append(("Error","An error occured trying to switch to legacy mode")) component.stop(["AllTorrents"]).addCallback(dolegacy) def _torrent_filter(self, idx, data): if data==FILTER.ALL: self.__status_dict = {} self._curr_filter = None elif data==FILTER.ACTIVE: self.__status_dict = {"state":"Active"} self._curr_filter = "Active" elif data==FILTER.DOWNLOADING: self.__status_dict = {"state":"Downloading"} self._curr_filter = "Downloading" elif data==FILTER.SEEDING: self.__status_dict = {"state":"Seeding"} self._curr_filter = "Seeding" elif data==FILTER.PAUSED: self.__status_dict = {"state":"Paused"} self._curr_filter = "Paused" elif data==FILTER.CHECKING: self.__status_dict = {"state":"Checking"} self._curr_filter = "Checking" elif data==FILTER.ERROR: self.__status_dict = {"state":"Error"} self._curr_filter = "Error" elif data==FILTER.QUEUED: self.__status_dict = {"state":"Queued"} self._curr_filter = "Queued" self._go_top = True return True def _show_torrent_filter_popup(self): self.popup = SelectablePopup(self,"Filter Torrents",self._torrent_filter) self.popup.add_line("_All",data=FILTER.ALL) self.popup.add_line("Ac_tive",data=FILTER.ACTIVE) self.popup.add_line("_Downloading",data=FILTER.DOWNLOADING,foreground="green") self.popup.add_line("_Seeding",data=FILTER.SEEDING,foreground="cyan") self.popup.add_line("_Paused",data=FILTER.PAUSED) self.popup.add_line("_Error",data=FILTER.ERROR,foreground="red") 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): 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) if succ_cnt != 0: msg += "\n \n{!success!}Successfully added %d torrent(s)"%succ_cnt self.report_message("Torrent Add Report",msg) def _do_add(self, result): log.debug("Adding Torrent(s): %s (dl path: %s) (paused: %d)",result["file"],result["path"],result["add_paused"]) ress = {"succ":0, "fail":0, "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.__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"]) else: fail_cb("Already in session (probably)",t_file,ress) add_torrent(result["file"],result,suc_cb,fail_cb,ress) def _show_torrent_add_popup(self): dl = "" ap = 1 try: dl = self.coreconfig["download_location"] except KeyError: pass try: if self.coreconfig["add_paused"]: ap = 0 except KeyError: pass self.popup = InputPopup(self,"Add Torrent (Esc to cancel)",close_cb=self._do_add) self.popup.add_text_input("Enter path to torrent file:","file") self.popup.add_text_input("Enter save path:","path",dl) self.popup.add_select_input("Add Paused:","add_paused",["Yes","No"],[True,False],ap) self.popup.add_spaces(1) self.popup.add_select_input("Path is:","path_type",["Auto","File","URL"],[0,1,2],0) def report_message(self,title,message): self.messages.append((title,message)) def clear_marks(self): self.marked = [] self.last_mark = -1 def set_popup(self,pu): self.popup = pu self.refresh() def refresh(self,lines=None): #log.error("ref") #import traceback #traceback.print_stack() # Something has requested we scroll to the top of the list if self._go_top: self.cursel = 1 self.curoff = 1 self._go_top = False # show a message popup if there's anything queued if self.popup == None and self.messages: title,msg = self.messages.popleft() self.popup = MessagePopup(self,title,msg) if not lines: self.stdscr.clear() # Update the status bars if self._curr_filter == None: self.add_string(0,self.statusbars.topbar) else: self.add_string(0,"%s {!filterstatus!}Current filter: %s"%(self.statusbars.topbar,self._curr_filter)) self.add_string(1,self.column_string) if self.entering_search: self.add_string(self.rows - 1,"{!black,white!}Search torrents: %s"%self.search_string) else: hstr = "%sPress [h] for help"%(" "*(self.cols - len(self.statusbars.bottombar) - 10)) self.add_string(self.rows - 1, "%s%s"%(self.statusbars.bottombar,hstr)) # add all the torrents if self.formatted_rows == []: msg = "No torrents match filter".center(self.cols) self.add_string(3, "{!info!}%s"%msg) elif self.formatted_rows: tidx = self.curoff currow = 2 if lines: todraw = [] for l in lines: todraw.append(self.formatted_rows[l]) lines.reverse() else: todraw = self.formatted_rows[tidx-1:] for row in todraw: # default style fg = "white" bg = "black" attr = None if lines: tidx = lines.pop()+1 currow = tidx-self.curoff+2 if tidx in self.marked: bg = "blue" attr = "bold" if tidx == self.cursel: bg = "white" attr = "bold" if tidx in self.marked: fg = "blue" else: fg = "black" if row[1] == "Downloading": fg = "green" elif row[1] == "Seeding": fg = "cyan" elif row[1] == "Error": fg = "red" elif row[1] == "Queued": fg = "yellow" elif row[1] == "Checking": fg = "blue" if attr: colorstr = "{!%s,%s,%s!}"%(fg,bg,attr) else: colorstr = "{!%s,%s!}"%(fg,bg) self.add_string(currow,"%s%s"%(colorstr,row[0]),trim=False) tidx += 1 currow += 1 if (currow > (self.rows - 2)): break else: self.add_string(1, "Waiting for torrents from core...") #self.stdscr.redrawwin() if self.entering_search: curses.curs_set(2) self.stdscr.move(self.rows-1,self.cursor+17) else: curses.curs_set(0) self.stdscr.noutrefresh() if self.popup: self.popup.refresh() curses.doupdate() def _mark_unmark(self,idx): if idx in self.marked: self.marked.remove(idx) self.last_mark = -1 else: self.marked.append(idx) self.last_mark = idx def __do_search(self): # search forward for the next torrent matching self.search_string for i,n in enumerate(self.torrent_names[self.cursel:]): if n.find(self.search_string) >= 0: self.cursel += (i+1) if ((self.curoff + self.rows - 5) < self.cursel): self.curoff = self.cursel - self.rows + 5 return def __update_search(self, c): if c == curses.KEY_BACKSPACE or c == 127: if self.search_string and self.cursor > 0: self.search_string = self.search_string[:self.cursor - 1] + self.search_string[self.cursor:] self.cursor-=1 elif c == curses.KEY_DC: if self.search_string and self.cursor < len(self.search_string): self.search_string = self.search_string[:self.cursor] + self.search_string[self.cursor+1:] elif c == curses.KEY_LEFT: self.cursor = max(0,self.cursor-1) elif c == curses.KEY_RIGHT: self.cursor = min(len(self.search_string),self.cursor+1) elif c == curses.KEY_HOME: self.cursor = 0 elif c == curses.KEY_END: self.cursor = len(self.search_string) elif c == 27: self.search_string = None self.entering_search = False elif c == 10 or c == curses.KEY_ENTER: self.entering_search = False if self.search_string: self.__do_search() else: self.search_string = None elif c > 31 and c < 256: stroke = chr(c) uchar = "" while not uchar: try: uchar = stroke.decode(self.encoding) except UnicodeDecodeError: c = self.stdscr.getch() stroke += chr(c) if uchar: if self.cursor == len(self.search_string): self.search_string += uchar else: # Insert into string self.search_string = self.search_string[:self.cursor] + uchar + self.search_string[self.cursor:] # Move the cursor forward self.cursor+=1 def _doRead(self): # Read the character effected_lines = None 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 if self.formatted_rows==None or self.popup: return elif self.entering_search: self.__update_search(c) self.refresh([]) return #log.error("pressed key: %d\n",c) #if c == 27: # handle escape # log.error("CANCEL") # Navigate the torrent list if c == curses.KEY_UP: if self.cursel == 1: return if not self._scroll_up(1): effected_lines = [self.cursel-1,self.cursel] elif c == curses.KEY_PPAGE: self._scroll_up(int(self.rows/2)) elif c == curses.KEY_DOWN: if self.cursel >= self.numtorrents: return if not self._scroll_down(1): effected_lines = [self.cursel-2,self.cursel-1] elif c == curses.KEY_NPAGE: self._scroll_down(int(self.rows/2)) elif c == curses.KEY_HOME: self._scroll_up(self.cursel) elif c == curses.KEY_END: self._scroll_down(self.numtorrents-self.cursel) elif c == curses.KEY_RIGHT: # We enter a new mode for the selected torrent here tid = self.current_torrent_id() if tid: self.show_torrent_details(tid) return # Enter Key elif (c == curses.KEY_ENTER or c == 10) and self.numtorrents: if self.cursel not in self.marked: self.marked.append(self.cursel) self.last_mark = self.cursel torrent_actions_popup(self,self._selected_torrent_ids(),details=True) return else: if c > 31 and c < 256: if chr(c) == '/': self.search_string = "" self.cursor = 0 self.entering_search = True elif chr(c) == 'n' and self.search_string: self.__do_search() elif chr(c) == 'j': if not self._scroll_up(1): effected_lines = [self.cursel-1,self.cursel] elif chr(c) == 'k': if not self._scroll_down(1): effected_lines = [self.cursel-2,self.cursel-1] elif chr(c) == 'i': cid = self.current_torrent_id() if cid: def cb(): self.__torrent_info_id = None self.popup = Popup(self,"Info",close_cb=cb) self.popup.add_line("Getting torrent info...") self.__torrent_info_id = cid elif chr(c) == 'm': self._mark_unmark(self.cursel) effected_lines = [self.cursel-1] elif chr(c) == 'M': if self.last_mark >= 0: if (self.cursel+1) > self.last_mark: mrange = range(self.last_mark,self.cursel+1) else: mrange = range(self.cursel-1,self.last_mark) self.marked.extend(mrange[1:]) effected_lines = mrange else: self._mark_unmark(self.cursel) effected_lines = [self.cursel-1] elif chr(c) == 'c': self.marked = [] self.last_mark = -1 elif chr(c) == 'a': self._show_torrent_add_popup() elif chr(c) == 'f': self._show_torrent_filter_popup() elif chr(c) == 'h': self.popup = Popup(self,"Help",init_lines=self.__help_lines) elif chr(c) == 'p': self.show_preferences() return elif chr(c) == 'e': self.__show_events() return elif chr(c) == 'l': self.__legacy_mode() return self.refresh(effected_lines)
class AllTorrents(BaseMode, component.Component): def __init__(self, stdscr, encoding=None): self.formatted_rows = None self.torrent_names = None self.cursel = 1 self.curoff = 1 # TODO: this should really be 0 indexed self.column_string = "" self.popup = None self.messages = deque() self.marked = [] self.last_mark = -1 self._sorted_ids = None self._go_top = False self._curr_filter = None self.entering_search = False self.search_string = None self.cursor = 0 self.coreconfig = component.get("ConsoleUI").coreconfig self.legacy_mode = None self.__status_dict = {} self.__torrent_info_id = None BaseMode.__init__(self, stdscr, encoding) component.Component.__init__(self, "AllTorrents", 1, depend=["SessionProxy"]) curses.curs_set(0) self.stdscr.notimeout(0) self.__split_help() self.update_config() component.start(["AllTorrents"]) self._info_fields = [ ("Name",None,("name",)), ("State", None, ("state",)), ("Down Speed", format_utils.format_speed, ("download_payload_rate",)), ("Up Speed", format_utils.format_speed, ("upload_payload_rate",)), ("Progress", format_utils.format_progress, ("progress",)), ("ETA", deluge.common.ftime, ("eta",)), ("Path", None, ("save_path",)), ("Downloaded",deluge.common.fsize,("all_time_download",)), ("Uploaded", deluge.common.fsize,("total_uploaded",)), ("Share Ratio", format_utils.format_float, ("ratio",)), ("Seeders",format_utils.format_seeds_peers,("num_seeds","total_seeds")), ("Peers",format_utils.format_seeds_peers,("num_peers","total_peers")), ("Active Time",deluge.common.ftime,("active_time",)), ("Seeding Time",deluge.common.ftime,("seeding_time",)), ("Date Added",deluge.common.fdate,("time_added",)), ("Availability", format_utils.format_float, ("distributed_copies",)), ("Pieces", format_utils.format_pieces, ("num_pieces","piece_length")), ] self.__status_keys = ["name","state","download_payload_rate","upload_payload_rate", "progress","eta","all_time_download","total_uploaded", "ratio", "num_seeds","total_seeds","num_peers","total_peers", "active_time", "seeding_time","time_added","distributed_copies", "num_pieces", "piece_length","save_path"] # component start/update def start(self): component.get("SessionProxy").get_torrents_status(self.__status_dict, self.__status_fields).addCallback(self.set_state,False) def update(self): component.get("SessionProxy").get_torrents_status(self.__status_dict, self.__status_fields).addCallback(self.set_state,True) if self.__torrent_info_id: component.get("SessionProxy").get_torrent_status(self.__torrent_info_id, self.__status_keys).addCallback(self._on_torrent_status) def update_config(self): self.config = ConfigManager("console.conf",DEFAULT_PREFS) self.__cols_to_show = [pref for pref in column_pref_names if self.config["show_%s"%pref]] self.__columns = [prefs_to_names[col] for col in self.__cols_to_show] self.__status_fields = column.get_required_fields(self.__columns) for rf in ["state","name","queue"]: # we always need these, even if we're not displaying them if not rf in self.__status_fields: self.__status_fields.append(rf) self.__update_columns() def __split_help(self): self.__help_lines = format_utils.wrap_string(HELP_STR,(self.cols/2)-2) def resume(self): self._go_top = True component.start(["AllTorrents"]) self.refresh() def __update_columns(self): self.column_widths = [self.config["%s_width"%c] for c in self.__cols_to_show] req = sum(filter(lambda x:x >= 0,self.column_widths)) if (req > self.cols): # can't satisfy requests, just spread out evenly cw = int(self.cols/len(self.__columns)) for i in range(0,len(self.column_widths)): self.column_widths[i] = cw else: rem = self.cols - req var_cols = len(filter(lambda x: x < 0,self.column_widths)) if (var_cols > 0): vw = int(rem/var_cols) for i in range(0, len(self.column_widths)): if (self.column_widths[i] < 0): self.column_widths[i] = vw self.column_string = "{!header!}%s"%("".join(["%s%s"%(self.__columns[i]," "*(self.column_widths[i]-len(self.__columns[i]))) for i in range(0,len(self.__columns))])) def set_state(self, state, refresh): self.curstate = state # cache in case we change sort order newnames = [] newrows = [] self._sorted_ids = self._sort_torrents(self.curstate) for torrent_id in self._sorted_ids: ts = self.curstate[torrent_id] newnames.append(ts["name"]) newrows.append((format_utils.format_row([column.get_column_value(name,ts) for name in self.__columns],self.column_widths),ts["state"])) self.numtorrents = len(state) self.formatted_rows = newrows self.torrent_names = newnames if refresh: self.refresh() def get_torrent_name(self, torrent_id): for p,i in enumerate(self._sorted_ids): if torrent_id == i: return self.torrent_names[p] return None def _scroll_up(self, by): prevoff = self.curoff self.cursel = max(self.cursel - by,1) if ((self.cursel - 1) < self.curoff): self.curoff = max(self.cursel - 1,1) return prevoff != self.curoff def _scroll_down(self, by): prevoff = self.curoff self.cursel = min(self.cursel + by,self.numtorrents) if ((self.curoff + self.rows - 5) < self.cursel): self.curoff = self.cursel - self.rows + 5 return prevoff != self.curoff def current_torrent_id(self): if self._sorted_ids: return self._sorted_ids[self.cursel-1] else: return None def _selected_torrent_ids(self): ret = [] for i in self.marked: ret.append(self._sorted_ids[i-1]) return ret def _on_torrent_status(self, state): if (self.popup): self.popup.clear() name = state["name"] off = int((self.cols/4)-(len(name)/2)) self.popup.set_title(name) for i,f in enumerate(self._info_fields): if f[1] != None: args = [] try: for key in f[2]: args.append(state[key]) except: log.debug("Could not get info field: %s",e) continue info = f[1](*args) else: info = state[f[2][0]] nl = len(f[0])+4 if (nl+len(info))>self.popup.width: self.popup.add_line("{!info!}%s: {!input!}%s"%(f[0],info[:(self.popup.width - nl)])) info = info[(self.popup.width - nl):] n = self.popup.width-3 chunks = [info[i:i+n] for i in xrange(0, len(info), n)] for c in chunks: self.popup.add_line(" %s"%c) else: self.popup.add_line("{!info!}%s: {!input!}%s"%(f[0],info)) self.refresh() else: self.__torrent_info_id = None def on_resize(self, *args): BaseMode.on_resize_norefresh(self, *args) self.__update_columns() self.__split_help() if self.popup: self.popup.handle_resize() self.refresh() def _queue_sort(self, v1, v2): if v1 == v2: return 0 if v2 < 0: return -1 if v1 < 0: return 1 if v1 > v2: return 1 if v2 > v1: return -1 def _sort_torrents(self, state): "sorts by queue #" return sorted(state,cmp=self._queue_sort,key=lambda s:state.get(s)["queue"]) def _format_queue(self, qnum): if (qnum >= 0): return "%d"%(qnum+1) else: return "" def show_torrent_details(self,tid): def dodeets(arg): if arg and True in arg[0]: self.stdscr.clear() component.get("ConsoleUI").set_mode(TorrentDetail(self,tid,self.stdscr,self.encoding)) else: self.messages.append(("Error","An error occured trying to display torrent details")) component.stop(["AllTorrents"]).addCallback(dodeets) def show_preferences(self): def _on_get_config(config): client.core.get_listen_port().addCallback(_on_get_listen_port,config) def _on_get_listen_port(port,config): client.core.get_cache_status().addCallback(_on_get_cache_status,port,config) def _on_get_cache_status(status,port,config): def doprefs(arg): if arg and True in arg[0]: self.stdscr.clear() component.get("ConsoleUI").set_mode(Preferences(self,config,self.config,port,status,self.stdscr,self.encoding)) else: self.messages.append(("Error","An error occured trying to display preferences")) component.stop(["AllTorrents"]).addCallback(doprefs) client.core.get_config().addCallback(_on_get_config) def __show_events(self): def doevents(arg): if arg and True in arg[0]: self.stdscr.clear() component.get("ConsoleUI").set_mode(EventView(self,self.stdscr,self.encoding)) else: self.messages.append(("Error","An error occured trying to display events")) component.stop(["AllTorrents"]).addCallback(doevents) def __legacy_mode(self): def dolegacy(arg): if arg and True in arg[0]: self.stdscr.clear() if not self.legacy_mode: self.legacy_mode = Legacy(self.stdscr,self.encoding) component.get("ConsoleUI").set_mode(self.legacy_mode) self.legacy_mode.refresh() curses.curs_set(2) else: self.messages.append(("Error","An error occured trying to switch to legacy mode")) component.stop(["AllTorrents"]).addCallback(dolegacy) def _torrent_filter(self, idx, data): if data==FILTER.ALL: self.__status_dict = {} self._curr_filter = None elif data==FILTER.ACTIVE: self.__status_dict = {"state":"Active"} self._curr_filter = "Active" elif data==FILTER.DOWNLOADING: self.__status_dict = {"state":"Downloading"} self._curr_filter = "Downloading" elif data==FILTER.SEEDING: self.__status_dict = {"state":"Seeding"} self._curr_filter = "Seeding" elif data==FILTER.PAUSED: self.__status_dict = {"state":"Paused"} self._curr_filter = "Paused" elif data==FILTER.CHECKING: self.__status_dict = {"state":"Checking"} self._curr_filter = "Checking" elif data==FILTER.ERROR: self.__status_dict = {"state":"Error"} self._curr_filter = "Error" elif data==FILTER.QUEUED: self.__status_dict = {"state":"Queued"} self._curr_filter = "Queued" self._go_top = True return True def _show_torrent_filter_popup(self): self.popup = SelectablePopup(self,"Filter Torrents",self._torrent_filter) self.popup.add_line("_All",data=FILTER.ALL) self.popup.add_line("Ac_tive",data=FILTER.ACTIVE) self.popup.add_line("_Downloading",data=FILTER.DOWNLOADING,foreground="green") self.popup.add_line("_Seeding",data=FILTER.SEEDING,foreground="cyan") self.popup.add_line("_Paused",data=FILTER.PAUSED) self.popup.add_line("_Error",data=FILTER.ERROR,foreground="red") 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): if fail_cnt == 0: self.report_message("Torrents Added","{!success!}Sucessfully 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) if succ_cnt != 0: msg += "\n \n{!success!}Sucessfully added %d torrent(s)"%succ_cnt self.report_message("Torrent Add Report",msg) def _do_add(self, result): log.debug("Adding Torrent(s): %s (dl path: %s) (paused: %d)",result["file"],result["path"],result["add_paused"]) ress = {"succ":0, "fail":0, "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.__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"]) else: fail_cb("Already in session (probably)",t_file,ress) add_torrent(result["file"],result,suc_cb,fail_cb,ress) def _show_torrent_add_popup(self): dl = "" ap = 1 try: dl = self.coreconfig["download_location"] except KeyError: pass try: if self.coreconfig["add_paused"]: ap = 0 except KeyError: pass self.popup = InputPopup(self,"Add Torrent (Esc to cancel)",close_cb=self._do_add) self.popup.add_text_input("Enter path to torrent file:","file") self.popup.add_text_input("Enter save path:","path",dl) self.popup.add_select_input("Add Paused:","add_paused",["Yes","No"],[True,False],ap) self.popup.add_spaces(1) self.popup.add_select_input("Path is:","path_type",["Auto","File","URL"],[0,1,2],0) def report_message(self,title,message): self.messages.append((title,message)) def clear_marks(self): self.marked = [] self.last_mark = -1 def set_popup(self,pu): self.popup = pu self.refresh() def refresh(self,lines=None): #log.error("ref") #import traceback #traceback.print_stack() # Something has requested we scroll to the top of the list if self._go_top: self.cursel = 1 self.curoff = 1 self._go_top = False # show a message popup if there's anything queued if self.popup == None and self.messages: title,msg = self.messages.popleft() self.popup = MessagePopup(self,title,msg) if not lines: self.stdscr.clear() # Update the status bars if self._curr_filter == None: self.add_string(0,self.statusbars.topbar) else: self.add_string(0,"%s {!filterstatus!}Current filter: %s"%(self.statusbars.topbar,self._curr_filter)) self.add_string(1,self.column_string) if self.entering_search: self.add_string(self.rows - 1,"{!black,white!}Search torrents: %s"%self.search_string) else: hstr = "%sPress [h] for help"%(" "*(self.cols - len(self.statusbars.bottombar) - 10)) self.add_string(self.rows - 1, "%s%s"%(self.statusbars.bottombar,hstr)) # add all the torrents if self.formatted_rows == []: msg = "No torrents match filter".center(self.cols) self.add_string(3, "{!info!}%s"%msg) elif self.formatted_rows: tidx = self.curoff currow = 2 if lines: todraw = [] for l in lines: todraw.append(self.formatted_rows[l]) lines.reverse() else: todraw = self.formatted_rows[tidx-1:] for row in todraw: # default style fg = "white" bg = "black" attr = None if lines: tidx = lines.pop()+1 currow = tidx-self.curoff+2 if tidx in self.marked: bg = "blue" attr = "bold" if tidx == self.cursel: bg = "white" attr = "bold" if tidx in self.marked: fg = "blue" else: fg = "black" if row[1] == "Downloading": fg = "green" elif row[1] == "Seeding": fg = "cyan" elif row[1] == "Error": fg = "red" elif row[1] == "Queued": fg = "yellow" elif row[1] == "Checking": fg = "blue" if attr: colorstr = "{!%s,%s,%s!}"%(fg,bg,attr) else: colorstr = "{!%s,%s!}"%(fg,bg) self.add_string(currow,"%s%s"%(colorstr,row[0]),trim=False) tidx += 1 currow += 1 if (currow > (self.rows - 2)): break else: self.add_string(1, "Waiting for torrents from core...") #self.stdscr.redrawwin() if self.entering_search: curses.curs_set(2) self.stdscr.move(self.rows-1,self.cursor+17) else: curses.curs_set(0) self.stdscr.noutrefresh() if self.popup: self.popup.refresh() curses.doupdate() def _mark_unmark(self,idx): if idx in self.marked: self.marked.remove(idx) self.last_mark = -1 else: self.marked.append(idx) self.last_mark = idx def __do_search(self): # search forward for the next torrent matching self.search_string for i,n in enumerate(self.torrent_names[self.cursel:]): if n.find(self.search_string) >= 0: self.cursel += (i+1) if ((self.curoff + self.rows - 5) < self.cursel): self.curoff = self.cursel - self.rows + 5 return def __update_search(self, c): if c == curses.KEY_BACKSPACE or c == 127: if self.search_string and self.cursor > 0: self.search_string = self.search_string[:self.cursor - 1] + self.search_string[self.cursor:] self.cursor-=1 elif c == curses.KEY_DC: if self.search_string and self.cursor < len(self.search_string): self.search_string = self.search_string[:self.cursor] + self.search_string[self.cursor+1:] elif c == curses.KEY_LEFT: self.cursor = max(0,self.cursor-1) elif c == curses.KEY_RIGHT: self.cursor = min(len(self.search_string),self.cursor+1) elif c == curses.KEY_HOME: self.cursor = 0 elif c == curses.KEY_END: self.cursor = len(self.search_string) elif c == 27: self.search_string = None self.entering_search = False elif c == 10 or c == curses.KEY_ENTER: self.entering_search = False if self.search_string: self.__do_search() else: self.search_string = None elif c > 31 and c < 256: stroke = chr(c) uchar = "" while not uchar: try: uchar = stroke.decode(self.encoding) except UnicodeDecodeError: c = self.stdscr.getch() stroke += chr(c) if uchar: if self.cursor == len(self.search_string): self.search_string += uchar else: # Insert into string self.search_string = self.search_string[:self.cursor] + uchar + self.search_string[self.cursor:] # Move the cursor forward self.cursor+=1 def _doRead(self): # Read the character effected_lines = None 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 if self.formatted_rows==None or self.popup: return elif self.entering_search: self.__update_search(c) self.refresh([]) return #log.error("pressed key: %d\n",c) #if c == 27: # handle escape # log.error("CANCEL") # Navigate the torrent list if c == curses.KEY_UP: if self.cursel == 1: return if not self._scroll_up(1): effected_lines = [self.cursel-1,self.cursel] elif c == curses.KEY_PPAGE: self._scroll_up(int(self.rows/2)) elif c == curses.KEY_DOWN: if self.cursel >= self.numtorrents: return if not self._scroll_down(1): effected_lines = [self.cursel-2,self.cursel-1] elif c == curses.KEY_NPAGE: self._scroll_down(int(self.rows/2)) elif c == curses.KEY_HOME: self._scroll_up(self.cursel) elif c == curses.KEY_END: self._scroll_down(self.numtorrents-self.cursel) elif c == curses.KEY_RIGHT: # We enter a new mode for the selected torrent here tid = self.current_torrent_id() if tid: self.show_torrent_details(tid) return # Enter Key elif (c == curses.KEY_ENTER or c == 10) and self.numtorrents: if self.cursel not in self.marked: self.marked.append(self.cursel) self.last_mark = self.cursel torrent_actions_popup(self,self._selected_torrent_ids(),details=True) return else: if c > 31 and c < 256: if chr(c) == '/': self.search_string = "" self.cursor = 0 self.entering_search = True elif chr(c) == 'n' and self.search_string: self.__do_search() elif chr(c) == 'j': if not self._scroll_up(1): effected_lines = [self.cursel-1,self.cursel] elif chr(c) == 'k': if not self._scroll_down(1): effected_lines = [self.cursel-2,self.cursel-1] elif chr(c) == 'i': cid = self.current_torrent_id() if cid: def cb(): self.__torrent_info_id = None self.popup = Popup(self,"Info",close_cb=cb) self.popup.add_line("Getting torrent info...") self.__torrent_info_id = cid elif chr(c) == 'm': self._mark_unmark(self.cursel) effected_lines = [self.cursel-1] elif chr(c) == 'M': if self.last_mark >= 0: if (self.cursel+1) > self.last_mark: mrange = range(self.last_mark,self.cursel+1) else: mrange = range(self.cursel-1,self.last_mark) self.marked.extend(mrange[1:]) effected_lines = mrange else: self._mark_unmark(self.cursel) effected_lines = [self.cursel-1] elif chr(c) == 'c': self.marked = [] self.last_mark = -1 elif chr(c) == 'a': self._show_torrent_add_popup() elif chr(c) == 'f': self._show_torrent_filter_popup() elif chr(c) == 'h': self.popup = Popup(self,"Help",init_lines=self.__help_lines) elif chr(c) == 'p': self.show_preferences() return elif chr(c) == 'e': self.__show_events() return elif chr(c) == 'l': self.__legacy_mode() return self.refresh(effected_lines)
class TorrentDetail(BaseMode, component.Component): def __init__(self, alltorrentmode, torrentid, stdscr, encoding=None): self.alltorrentmode = alltorrentmode self.torrentid = torrentid self.torrent_state = None self.popup = None self.messages = deque() self._status_keys = ["files", "name","state","download_payload_rate","upload_payload_rate", "progress","eta","all_time_download","total_uploaded", "ratio", "num_seeds","total_seeds","num_peers","total_peers", "active_time", "seeding_time","time_added","distributed_copies", "num_pieces", "piece_length","save_path","file_progress","file_priorities","message"] self._info_fields = [ ("Name",None,("name",)), ("State", None, ("state",)), ("Status",None,("message",)), ("Down Speed", format_utils.format_speed, ("download_payload_rate",)), ("Up Speed", format_utils.format_speed, ("upload_payload_rate",)), ("Progress", format_utils.format_progress, ("progress",)), ("ETA", deluge.common.ftime, ("eta",)), ("Path", None, ("save_path",)), ("Downloaded",deluge.common.fsize,("all_time_download",)), ("Uploaded", deluge.common.fsize,("total_uploaded",)), ("Share Ratio", lambda x:x < 0 and "∞" or "%.3f"%x, ("ratio",)), ("Seeders",format_utils.format_seeds_peers,("num_seeds","total_seeds")), ("Peers",format_utils.format_seeds_peers,("num_peers","total_peers")), ("Active Time",deluge.common.ftime,("active_time",)), ("Seeding Time",deluge.common.ftime,("seeding_time",)), ("Date Added",deluge.common.fdate,("time_added",)), ("Availability", lambda x:x < 0 and "∞" or "%.3f"%x, ("distributed_copies",)), ("Pieces", format_utils.format_pieces, ("num_pieces","piece_length")), ] self.file_list = None self.current_file = None self.current_file_idx = 0 self.file_limit = maxint self.file_off = 0 self.more_to_draw = False self.column_string = "" self.files_sep = None self.marked = {} BaseMode.__init__(self, stdscr, encoding) component.Component.__init__(self, "TorrentDetail", 1, depend=["SessionProxy"]) self.__split_help() self.column_names = ["Filename", "Size", "Progress", "Priority"] self.__update_columns() component.start(["TorrentDetail"]) curses.curs_set(0) self.stdscr.notimeout(0) # component start/update def start(self): component.get("SessionProxy").get_torrent_status(self.torrentid, self._status_keys).addCallback(self.set_state) def update(self): component.get("SessionProxy").get_torrent_status(self.torrentid, self._status_keys).addCallback(self.set_state) def set_state(self, state): log.debug("got state") need_prio_update = False if not self.file_list: # don't keep getting the files once we've got them once if state.get("files"): self.files_sep = "{!green,black,bold,underline!}%s"%(("Files (torrent has %d files)"%len(state["files"])).center(self.cols)) self.file_list,self.file_dict = self.build_file_list(state["files"],state["file_progress"],state["file_priorities"]) self._status_keys.remove("files") else: self.files_sep = "{!green,black,bold,underline!}%s"%(("Files (File list unknown)").center(self.cols)) need_prio_update = True self.__fill_progress(self.file_list,state["file_progress"]) for i,prio in enumerate(state["file_priorities"]): if self.file_dict[i][6] != prio: need_prio_update = True self.file_dict[i][6] = prio if need_prio_update: self.__fill_prio(self.file_list) del state["file_progress"] del state["file_priorities"] self.torrent_state = state self.refresh() def __split_help(self): self.__help_lines = format_utils.wrap_string(HELP_STR,(self.cols/2)-2) # split file list into directory tree. this function assumes all files in a # particular directory are returned together. it won't work otherwise. # returned list is a list of lists of the form: # [file/dir_name,index,size,children,expanded,progress,priority] # for directories index values count down from maxint (for marking usage), # for files the index is the value returned in the # state object for use with other libtorrent calls (i.e. setting prio) # # Also returns a dictionary that maps index values to the file leaves # for fast updating of progress and priorities def build_file_list(self, file_tuples,prog,prio): ret = [] retdict = {} diridx = maxint for f in file_tuples: cur = ret ps = f["path"].split("/") fin = ps[-1] for p in ps: if not cur or p != cur[-1][0]: cl = [] if p == fin: ent = [p,f["index"],f["size"],cl,False, format_utils.format_progress(prog[f["index"]]*100), prio[f["index"]]] retdict[f["index"]] = ent else: ent = [p,diridx,-1,cl,False,0,-1] retdict[diridx] = ent diridx-=1 cur.append(ent) cur = cl else: cur = cur[-1][3] self.__build_sizes(ret) self.__fill_progress(ret,prog) return (ret,retdict) # fill in the sizes of the directory entries based on their children def __build_sizes(self, fs): ret = 0 for f in fs: if f[2] == -1: val = self.__build_sizes(f[3]) ret += val f[2] = val else: ret += f[2] return ret # fills in progress fields in all entries based on progs # returns the # of bytes complete in all the children of fs def __fill_progress(self,fs,progs): if not progs: return 0 tb = 0 for f in fs: if f[3]: # dir, has some children bd = self.__fill_progress(f[3],progs) f[5] = format_utils.format_progress((bd/f[2])*100) else: # file, update own prog and add to total bd = f[2]*progs[f[1]] f[5] = format_utils.format_progress(progs[f[1]]*100) tb += bd return tb def __fill_prio(self,fs): for f in fs: if f[3]: # dir, so fill in children and compute our prio self.__fill_prio(f[3]) s = set([e[6] for e in f[3]]) # pull out all child prios and turn into a set if len(s) > 1: f[6] = -2 # mixed else: f[6] = s.pop() def __update_columns(self): self.column_widths = [-1,15,15,20] req = sum(filter(lambda x:x >= 0,self.column_widths)) if (req > self.cols): # can't satisfy requests, just spread out evenly cw = int(self.cols/len(self.column_names)) for i in range(0,len(self.column_widths)): self.column_widths[i] = cw else: rem = self.cols - req var_cols = len(filter(lambda x: x < 0,self.column_widths)) vw = int(rem/var_cols) for i in range(0, len(self.column_widths)): if (self.column_widths[i] < 0): self.column_widths[i] = vw self.column_string = "{!green,black,bold!}%s"%("".join(["%s%s"%(self.column_names[i]," "*(self.column_widths[i]-len(self.column_names[i]))) for i in range(0,len(self.column_names))])) def report_message(self,title,message): self.messages.append((title,message)) def clear_marks(self): self.marked = {} def set_popup(self,pu): self.popup = pu self.refresh() def draw_files(self,files,depth,off,idx): for fl in files: # kick out if we're going to draw too low on the screen if (off >= self.rows-1): self.more_to_draw = True return -1,-1 self.file_limit = idx if idx >= self.file_off: # set fg/bg colors based on if we are selected/marked or not # default values fg = "white" bg = "black" if fl[1] in self.marked: bg = "blue" if idx == self.current_file_idx: self.current_file = fl bg = "white" if fl[1] in self.marked: fg = "blue" else: fg = "black" color_string = "{!%s,%s!}"%(fg,bg) #actually draw the dir/file string if fl[3] and fl[4]: # this is an expanded directory xchar = 'v' elif fl[3]: # collapsed directory xchar = '>' else: # file xchar = '-' r = format_utils.format_row(["%s%s %s"%(" "*depth,xchar,fl[0]), deluge.common.fsize(fl[2]),fl[5], format_utils.format_priority(fl[6])], self.column_widths) self.add_string(off,"%s%s"%(color_string,r),trim=False) off += 1 if fl[3] and fl[4]: # recurse if we have children and are expanded off,idx = self.draw_files(fl[3],depth+1,off,idx+1) if off < 0: return (off,idx) else: idx += 1 return (off,idx) def on_resize(self, *args): BaseMode.on_resize_norefresh(self, *args) self.__update_columns() self.__split_help() if self.popup: self.popup.handle_resize() self.refresh() def refresh(self,lines=None): # show a message popup if there's anything queued if self.popup == None and self.messages: title,msg = self.messages.popleft() self.popup = MessagePopup(self,title,msg) # Update the status bars self.stdscr.clear() self.add_string(0,self.statusbars.topbar) hstr = "%sPress [h] for help"%(" "*(self.cols - len(self.statusbars.bottombar) - 10)) self.add_string(self.rows - 1, "%s%s"%(self.statusbars.bottombar,hstr)) if self.files_sep: self.add_string((self.rows/2)-1,self.files_sep) off = 1 if self.torrent_state: for f in self._info_fields: if off >= (self.rows/2): break if f[1] != None: args = [] try: for key in f[2]: args.append(self.torrent_state[key]) except: log.debug("Could not get info field: %s",e) continue info = f[1](*args) else: info = self.torrent_state[f[2][0]] self.add_string(off,"{!info!}%s: {!input!}%s"%(f[0],info)) off += 1 else: self.add_string(1, "Waiting for torrent state") off = self.rows/2 self.add_string(off,self.column_string) if self.file_list: off += 1 self.more_to_draw = False self.draw_files(self.file_list,0,off,0) #self.stdscr.redrawwin() self.stdscr.noutrefresh() if self.popup: self.popup.refresh() curses.doupdate() # expand or collapse the current file def expcol_cur_file(self): self.current_file[4] = not self.current_file[4] self.refresh() def file_list_down(self): if (self.current_file_idx + 1) > self.file_limit: if self.more_to_draw: self.current_file_idx += 1 self.file_off += 1 else: return else: self.current_file_idx += 1 self.refresh() def file_list_up(self): self.current_file_idx = max(0,self.current_file_idx-1) self.file_off = min(self.file_off,self.current_file_idx) self.refresh() def back_to_overview(self): component.stop(["TorrentDetail"]) component.deregister(self) self.stdscr.clear() component.get("ConsoleUI").set_mode(self.alltorrentmode) self.alltorrentmode.resume() # build list of priorities for all files in the torrent # based on what is currently selected and a selected priority. def build_prio_list(self, files, ret_list, parent_prio, selected_prio): # has a priority been set on my parent (if so, I inherit it) for f in files: if f[3]: # dir, check if i'm setting on whole dir, then recurse if f[1] in self.marked: # marked, recurse and update all children with new prio parent_prio = selected_prio self.build_prio_list(f[3],ret_list,parent_prio,selected_prio) parent_prio = -1 else: # not marked, just recurse self.build_prio_list(f[3],ret_list,parent_prio,selected_prio) else: # file, need to add to list if f[1] in self.marked or parent_prio >= 0: # selected (or parent selected), use requested priority ret_list.append((f[1],selected_prio)) else: # not selected, just keep old priority ret_list.append((f[1],f[6])) def do_priority(self, idx, data): plist = [] self.build_prio_list(self.file_list,plist,-1,data) plist.sort() priorities = [p[1] for p in plist] log.debug("priorities: %s", priorities) client.core.set_torrent_file_priorities(self.torrentid, priorities) if len(self.marked) == 1: self.marked = {} return True # show popup for priority selections def show_priority_popup(self): if self.marked: self.popup = SelectablePopup(self,"Set File Priority",self.do_priority) self.popup.add_line("_Do Not Download",data=deluge.common.FILE_PRIORITY["Do Not Download"]) self.popup.add_line("_Normal Priority",data=deluge.common.FILE_PRIORITY["Normal Priority"]) self.popup.add_line("_High Priority",data=deluge.common.FILE_PRIORITY["High Priority"]) self.popup.add_line("H_ighest Priority",data=deluge.common.FILE_PRIORITY["Highest Priority"]) def __mark_unmark(self,idx): if idx in self.marked: del self.marked[idx] else: self.marked[idx] = True 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 if c == 27 or c == curses.KEY_LEFT: self.back_to_overview() return if not self.torrent_state: # actions below only makes sense if there is a torrent state return # Navigate the torrent list if c == curses.KEY_UP: self.file_list_up() elif c == curses.KEY_PPAGE: pass elif c == curses.KEY_DOWN: self.file_list_down() elif c == curses.KEY_NPAGE: pass # Enter Key elif c == curses.KEY_ENTER or c == 10: self.marked[self.current_file[1]] = True self.show_priority_popup() # space elif c == 32: self.expcol_cur_file() else: if c > 31 and c < 256: if chr(c) == 'm': if self.current_file: self.__mark_unmark(self.current_file[1]) elif chr(c) == 'c': self.marked = {} elif chr(c) == 'a': torrent_actions_popup(self,[self.torrentid],details=False) return elif chr(c) == 'h': self.popup = Popup(self,"Help",init_lines=self.__help_lines) self.refresh()
class TorrentDetail(BaseMode, component.Component): def __init__(self, alltorrentmode, torrentid, stdscr, encoding=None): self.alltorrentmode = alltorrentmode self.torrentid = torrentid self.torrent_state = None self.popup = None self.messages = deque() self._status_keys = [ "files", "name", "state", "download_payload_rate", "upload_payload_rate", "progress", "eta", "all_time_download", "total_uploaded", "ratio", "num_seeds", "total_seeds", "num_peers", "total_peers", "active_time", "seeding_time", "time_added", "distributed_copies", "num_pieces", "piece_length", "save_path", "file_progress", "file_priorities", "message" ] self._info_fields = [ ("Name", None, ("name", )), ("State", None, ("state", )), ("Status", None, ("message", )), ("Down Speed", format_utils.format_speed, ("download_payload_rate", )), ("Up Speed", format_utils.format_speed, ("upload_payload_rate", )), ("Progress", format_utils.format_progress, ("progress", )), ("ETA", deluge.common.ftime, ("eta", )), ("Path", None, ("save_path", )), ("Downloaded", deluge.common.fsize, ("all_time_download", )), ("Uploaded", deluge.common.fsize, ("total_uploaded", )), ("Share Ratio", lambda x: x < 0 and "∞" or "%.3f" % x, ("ratio", )), ("Seeders", format_utils.format_seeds_peers, ("num_seeds", "total_seeds")), ("Peers", format_utils.format_seeds_peers, ("num_peers", "total_peers")), ("Active Time", deluge.common.ftime, ("active_time", )), ("Seeding Time", deluge.common.ftime, ("seeding_time", )), ("Date Added", deluge.common.fdate, ("time_added", )), ("Availability", lambda x: x < 0 and "∞" or "%.3f" % x, ("distributed_copies", )), ("Pieces", format_utils.format_pieces, ("num_pieces", "piece_length")), ] self.file_list = None self.current_file = None self.current_file_idx = 0 self.file_limit = maxint self.file_off = 0 self.more_to_draw = False self.column_string = "" self.files_sep = None self.marked = {} BaseMode.__init__(self, stdscr, encoding) component.Component.__init__(self, "TorrentDetail", 1, depend=["SessionProxy"]) self.__split_help() self.column_names = ["Filename", "Size", "Progress", "Priority"] self.__update_columns() component.start(["TorrentDetail"]) curses.curs_set(0) self.stdscr.notimeout(0) # component start/update def start(self): component.get("SessionProxy").get_torrent_status( self.torrentid, self._status_keys).addCallback(self.set_state) def update(self): component.get("SessionProxy").get_torrent_status( self.torrentid, self._status_keys).addCallback(self.set_state) def set_state(self, state): log.debug("got state") need_prio_update = False if not self.file_list: # don't keep getting the files once we've got them once if state.get("files"): self.files_sep = "{!green,black,bold,underline!}%s" % ( ("Files (torrent has %d files)" % len(state["files"])).center(self.cols)) self.file_list, self.file_dict = self.build_file_list( state["files"], state["file_progress"], state["file_priorities"]) self._status_keys.remove("files") else: self.files_sep = "{!green,black,bold,underline!}%s" % ( ("Files (File list unknown)").center(self.cols)) need_prio_update = True self.__fill_progress(self.file_list, state["file_progress"]) for i, prio in enumerate(state["file_priorities"]): if self.file_dict[i][6] != prio: need_prio_update = True self.file_dict[i][6] = prio if need_prio_update: self.__fill_prio(self.file_list) del state["file_progress"] del state["file_priorities"] self.torrent_state = state self.refresh() def __split_help(self): self.__help_lines = format_utils.wrap_string(HELP_STR, (self.cols / 2) - 2) # split file list into directory tree. this function assumes all files in a # particular directory are returned together. it won't work otherwise. # returned list is a list of lists of the form: # [file/dir_name,index,size,children,expanded,progress,priority] # for directories index values count down from maxint (for marking usage), # for files the index is the value returned in the # state object for use with other libtorrent calls (i.e. setting prio) # # Also returns a dictionary that maps index values to the file leaves # for fast updating of progress and priorities def build_file_list(self, file_tuples, prog, prio): ret = [] retdict = {} diridx = maxint for f in file_tuples: cur = ret ps = f["path"].split("/") fin = ps[-1] for p in ps: if not cur or p != cur[-1][0]: cl = [] if p == fin: ent = [ p, f["index"], f["size"], cl, False, format_utils.format_progress(prog[f["index"]] * 100), prio[f["index"]] ] retdict[f["index"]] = ent else: ent = [p, diridx, -1, cl, False, 0, -1] retdict[diridx] = ent diridx -= 1 cur.append(ent) cur = cl else: cur = cur[-1][3] self.__build_sizes(ret) self.__fill_progress(ret, prog) return (ret, retdict) # fill in the sizes of the directory entries based on their children def __build_sizes(self, fs): ret = 0 for f in fs: if f[2] == -1: val = self.__build_sizes(f[3]) ret += val f[2] = val else: ret += f[2] return ret # fills in progress fields in all entries based on progs # returns the # of bytes complete in all the children of fs def __fill_progress(self, fs, progs): if not progs: return 0 tb = 0 for f in fs: if f[3]: # dir, has some children bd = self.__fill_progress(f[3], progs) f[5] = format_utils.format_progress((bd / f[2]) * 100) else: # file, update own prog and add to total bd = f[2] * progs[f[1]] f[5] = format_utils.format_progress(progs[f[1]] * 100) tb += bd return tb def __fill_prio(self, fs): for f in fs: if f[3]: # dir, so fill in children and compute our prio self.__fill_prio(f[3]) s = set([e[6] for e in f[3] ]) # pull out all child prios and turn into a set if len(s) > 1: f[6] = -2 # mixed else: f[6] = s.pop() def __update_columns(self): self.column_widths = [-1, 15, 15, 20] req = sum(filter(lambda x: x >= 0, self.column_widths)) if (req > self.cols): # can't satisfy requests, just spread out evenly cw = int(self.cols / len(self.column_names)) for i in range(0, len(self.column_widths)): self.column_widths[i] = cw else: rem = self.cols - req var_cols = len(filter(lambda x: x < 0, self.column_widths)) vw = int(rem / var_cols) for i in range(0, len(self.column_widths)): if (self.column_widths[i] < 0): self.column_widths[i] = vw self.column_string = "{!green,black,bold!}%s" % ("".join([ "%s%s" % (self.column_names[i], " " * (self.column_widths[i] - len(self.column_names[i]))) for i in range(0, len(self.column_names)) ])) def report_message(self, title, message): self.messages.append((title, message)) def clear_marks(self): self.marked = {} def set_popup(self, pu): self.popup = pu self.refresh() def draw_files(self, files, depth, off, idx): for fl in files: # kick out if we're going to draw too low on the screen if (off >= self.rows - 1): self.more_to_draw = True return -1, -1 self.file_limit = idx if idx >= self.file_off: # set fg/bg colors based on if we are selected/marked or not # default values fg = "white" bg = "black" if fl[1] in self.marked: bg = "blue" if idx == self.current_file_idx: self.current_file = fl bg = "white" if fl[1] in self.marked: fg = "blue" else: fg = "black" color_string = "{!%s,%s!}" % (fg, bg) #actually draw the dir/file string if fl[3] and fl[4]: # this is an expanded directory xchar = 'v' elif fl[3]: # collapsed directory xchar = '>' else: # file xchar = '-' r = format_utils.format_row([ "%s%s %s" % (" " * depth, xchar, fl[0]), deluge.common.fsize(fl[2]), fl[5], format_utils.format_priority(fl[6]) ], self.column_widths) self.add_string(off, "%s%s" % (color_string, r), trim=False) off += 1 if fl[3] and fl[4]: # recurse if we have children and are expanded off, idx = self.draw_files(fl[3], depth + 1, off, idx + 1) if off < 0: return (off, idx) else: idx += 1 return (off, idx) def on_resize(self, *args): BaseMode.on_resize_norefresh(self, *args) self.__update_columns() self.__split_help() if self.popup: self.popup.handle_resize() self.refresh() def refresh(self, lines=None): # show a message popup if there's anything queued if self.popup == None and self.messages: title, msg = self.messages.popleft() self.popup = MessagePopup(self, title, msg) # Update the status bars self.stdscr.clear() self.add_string(0, self.statusbars.topbar) hstr = "%sPress [h] for help" % ( " " * (self.cols - len(self.statusbars.bottombar) - 10)) self.add_string(self.rows - 1, "%s%s" % (self.statusbars.bottombar, hstr)) if self.files_sep: self.add_string((self.rows / 2) - 1, self.files_sep) off = 1 if self.torrent_state: for f in self._info_fields: if off >= (self.rows / 2): break if f[1] != None: args = [] try: for key in f[2]: args.append(self.torrent_state[key]) except: log.debug("Could not get info field: %s", e) continue info = f[1](*args) else: info = self.torrent_state[f[2][0]] self.add_string(off, "{!info!}%s: {!input!}%s" % (f[0], info)) off += 1 else: self.add_string(1, "Waiting for torrent state") off = self.rows / 2 self.add_string(off, self.column_string) if self.file_list: off += 1 self.more_to_draw = False self.draw_files(self.file_list, 0, off, 0) #self.stdscr.redrawwin() self.stdscr.noutrefresh() if self.popup: self.popup.refresh() curses.doupdate() # expand or collapse the current file def expcol_cur_file(self): self.current_file[4] = not self.current_file[4] self.refresh() def file_list_down(self): if (self.current_file_idx + 1) > self.file_limit: if self.more_to_draw: self.current_file_idx += 1 self.file_off += 1 else: return else: self.current_file_idx += 1 self.refresh() def file_list_up(self): self.current_file_idx = max(0, self.current_file_idx - 1) self.file_off = min(self.file_off, self.current_file_idx) self.refresh() def back_to_overview(self): component.stop(["TorrentDetail"]) component.deregister(self) self.stdscr.clear() component.get("ConsoleUI").set_mode(self.alltorrentmode) self.alltorrentmode.resume() # build list of priorities for all files in the torrent # based on what is currently selected and a selected priority. def build_prio_list(self, files, ret_list, parent_prio, selected_prio): # has a priority been set on my parent (if so, I inherit it) for f in files: if f[3]: # dir, check if i'm setting on whole dir, then recurse if f[1] in self.marked: # marked, recurse and update all children with new prio parent_prio = selected_prio self.build_prio_list(f[3], ret_list, parent_prio, selected_prio) parent_prio = -1 else: # not marked, just recurse self.build_prio_list(f[3], ret_list, parent_prio, selected_prio) else: # file, need to add to list if f[1] in self.marked or parent_prio >= 0: # selected (or parent selected), use requested priority ret_list.append((f[1], selected_prio)) else: # not selected, just keep old priority ret_list.append((f[1], f[6])) def do_priority(self, idx, data): plist = [] self.build_prio_list(self.file_list, plist, -1, data) plist.sort() priorities = [p[1] for p in plist] log.debug("priorities: %s", priorities) client.core.set_torrent_file_priorities(self.torrentid, priorities) if len(self.marked) == 1: self.marked = {} return True # show popup for priority selections def show_priority_popup(self): if self.marked: self.popup = SelectablePopup(self, "Set File Priority", self.do_priority) self.popup.add_line( "_Do Not Download", data=deluge.common.FILE_PRIORITY["Do Not Download"]) self.popup.add_line( "_Normal Priority", data=deluge.common.FILE_PRIORITY["Normal Priority"]) self.popup.add_line( "_High Priority", data=deluge.common.FILE_PRIORITY["High Priority"]) self.popup.add_line( "H_ighest Priority", data=deluge.common.FILE_PRIORITY["Highest Priority"]) def __mark_unmark(self, idx): if idx in self.marked: del self.marked[idx] else: self.marked[idx] = True 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 if c == 27 or c == curses.KEY_LEFT: self.back_to_overview() return if not self.torrent_state: # actions below only makes sense if there is a torrent state return # Navigate the torrent list if c == curses.KEY_UP: self.file_list_up() elif c == curses.KEY_PPAGE: pass elif c == curses.KEY_DOWN: self.file_list_down() elif c == curses.KEY_NPAGE: pass # Enter Key elif c == curses.KEY_ENTER or c == 10: self.marked[self.current_file[1]] = True self.show_priority_popup() # space elif c == 32: self.expcol_cur_file() else: if c > 31 and c < 256: if chr(c) == 'm': if self.current_file: self.__mark_unmark(self.current_file[1]) elif chr(c) == 'c': self.marked = {} elif chr(c) == 'a': torrent_actions_popup(self, [self.torrentid], details=False) return elif chr(c) == 'h': self.popup = Popup(self, "Help", init_lines=self.__help_lines) self.refresh()