deluge/deluge/ui/console/modes/alltorrents.py
2011-02-15 19:10:51 +01:00

641 lines
23 KiB
Python

# -*- coding: utf-8 -*-
#
# alltorrens.py
#
# Copyright (C) 2011 Nick Lanham <nick@afternight.org>
#
# 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
from deluge.ui.client import client
from collections import deque
from deluge.ui.sessionproxy import SessionProxy
from popup import Popup,SelectablePopup,MessagePopup
from add_util import add_torrent
from input_popup import InputPopup
from torrentdetail import TorrentDetail
from preferences import Preferences
from torrent_actions import torrent_actions_popup
from eventview import EventView
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 = \
"""This screen shows an overview of the current torrents Deluge is managing.
The currently selected torrent is indicated by having a white background.
You can change the selected torrent using the up/down arrows or the
PgUp/Pg keys.
Operations can be performed on multiple torrents by marking them and
then hitting Enter. See below for the keys used to mark torrents.
You can scroll a popup window that doesn't fit its content (like
this one) using the up/down arrows.
All popup windows can be closed/canceled by hitting the Esc key
(you might need to wait a second for an Esc to register)
The actions you can perform and the keys to perform them are as follows:
'h' - Show this help
'a' - Add a torrent
'p' - View/Set preferences
'f' - Show only torrents in a certain state
(Will open a popup where you can select the state you want to see)
'i' - Show more detailed information about the current selected torrent
'Q' - quit
'm' - Mark a torrent
'M' - Mark all torrents between currently selected torrent
and last marked torrent
'c' - Un-mark all torrents
Right Arrow - Torrent Detail Mode. This includes more detailed information
about the currently selected torrent, as well as a view of the
files in the torrent and the ability to set file priorities.
Enter - Show torrent actions popup. Here you can do things like
pause/resume, remove, recheck and so one. These actions
apply to all currently marked torrents. The currently
selected torrent is automatically marked when you press enter.
'q'/Esc - Close a popup
"""
HELP_LINES = HELP_STR.split('\n')
class FILTER:
ALL=0
ACTIVE=1
DOWNLOADING=2
SEEDING=3
PAUSED=4
CHECKING=5
ERROR=6
QUEUED=7
class StateUpdater(component.Component):
def __init__(self, cb, sf,tcb):
component.Component.__init__(self, "AllTorrentsStateUpdater", 1, depend=["SessionProxy"])
self._status_cb = cb
self._status_fields = sf
self.status_dict = {}
self._torrent_cb = tcb
self._torrent_to_update = None
def set_torrent_to_update(self, tid, keys):
self._torrent_to_update = tid
self._torrent_keys = keys
def start(self):
component.get("SessionProxy").get_torrents_status(self.status_dict, self._status_fields).addCallback(self._on_torrents_status,False)
def update(self):
component.get("SessionProxy").get_torrents_status(self.status_dict, self._status_fields).addCallback(self._on_torrents_status,True)
if self._torrent_to_update:
component.get("SessionProxy").get_torrent_status(self._torrent_to_update, self._torrent_keys).addCallback(self._torrent_cb)
def _on_torrents_status(self, state, refresh):
self._status_cb(state,refresh)
class AllTorrents(BaseMode):
def __init__(self, stdscr, encoding=None):
self.formatted_rows = 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.coreconfig = component.get("ConsoleUI").coreconfig
BaseMode.__init__(self, stdscr, encoding)
curses.curs_set(0)
self.stdscr.notimeout(0)
self._status_fields = ["queue","name","total_wanted","state","progress","num_seeds","total_seeds",
"num_peers","total_peers","download_payload_rate", "upload_payload_rate"]
self.updater = StateUpdater(self.set_state,self._status_fields,self._on_torrent_status)
self.column_names = ["#", "Name","Size","State","Progress","Seeders","Peers","Down Speed","Up Speed"]
self._update_columns()
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", 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._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"]
def resume(self):
component.start(["AllTorrentsStateUpdater"])
self.refresh()
def _update_columns(self):
self.column_widths = [5,-1,15,13,10,10,10,15,15]
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 = "{!header!}%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 set_state(self, state, refresh):
self.curstate = state # cache in case we change sort order
newrows = []
self._sorted_ids = self._sort_torrents(self.curstate)
for torrent_id in self._sorted_ids:
ts = self.curstate[torrent_id]
newrows.append((format_utils.format_row([self._format_queue(ts["queue"]),
ts["name"],
"%s"%deluge.common.fsize(ts["total_wanted"]),
ts["state"],
format_utils.format_progress(ts["progress"]),
format_utils.format_seeds_peers(ts["num_seeds"],ts["total_seeds"]),
format_utils.format_seeds_peers(ts["num_peers"],ts["total_peers"]),
format_utils.format_speed(ts["download_payload_rate"]),
format_utils.format_speed(ts["upload_payload_rate"])
],self.column_widths),ts["state"]))
self.numtorrents = len(state)
self.formatted_rows = newrows
if refresh:
self.refresh()
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]]
self.popup.add_line("{!info!}%s: {!input!}%s"%(f[0],info))
self.refresh()
else:
self.updater.set_torrent_to_update(None,None)
def on_resize(self, *args):
BaseMode.on_resize_norefresh(self, *args)
self._update_columns()
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):
component.stop(["AllTorrentsStateUpdater"])
self.stdscr.clear()
td = TorrentDetail(self,tid,self.stdscr,self.encoding)
component.get("ConsoleUI").set_mode(td)
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):
component.stop(["AllTorrentsStateUpdater"])
self.stdscr.clear()
prefs = Preferences(self,config,port,status,self.stdscr,self.encoding)
component.get("ConsoleUI").set_mode(prefs)
client.core.get_config().addCallback(_on_get_config)
def __show_events(self):
component.stop(["AllTorrentsStateUpdater"])
self.stdscr.clear()
ev = EventView(self,self.stdscr,self.encoding)
component.get("ConsoleUI").set_mode(ev)
def _torrent_filter(self, idx, data):
if data==FILTER.ALL:
self.updater.status_dict = {}
self._curr_filter = None
elif data==FILTER.ACTIVE:
self.updater.status_dict = {"state":"Active"}
self._curr_filter = "Active"
elif data==FILTER.DOWNLOADING:
self.updater.status_dict = {"state":"Downloading"}
self._curr_filter = "Downloading"
elif data==FILTER.SEEDING:
self.updater.status_dict = {"state":"Seeding"}
self._curr_filter = "Seeding"
elif data==FILTER.PAUSED:
self.updater.status_dict = {"state":"Paused"}
self._curr_filter = "Paused"
elif data==FILTER.CHECKING:
self.updater.status_dict = {"state":"Checking"}
self._curr_filter = "Checking"
elif data==FILTER.ERROR:
self.updater.status_dict = {"state":"Error"}
self._curr_filter = "Error"
elif data==FILTER.QUEUED:
self.updater.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 _do_add(self, result):
log.debug("Adding Torrent: %s (dl path: %s) (paused: %d)",result["file"],result["path"],result["add_paused"])
def suc_cb(msg):
self.report_message("Torrent Added",msg)
def fail_cb(msg):
self.report_message("Failed To Add Torrent",msg)
add_torrent(result["file"],result,suc_cb,fail_cb)
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)
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)
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]))
tidx += 1
currow += 1
if (currow > (self.rows - 2)):
break
else:
self.add_string(1, "Waiting for torrents from core...")
#self.stdscr.redrawwin()
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 _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
#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_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:
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) == '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:
self.popup = Popup(self,"Info",close_cb=lambda:self.updater.set_torrent_to_update(None,None))
self.popup.add_line("Getting torrent info...")
self.updater.set_torrent_to_update(cid,self._status_keys)
elif chr(c) == 'm':
self._mark_unmark(self.cursel)
effected_lines = [self.cursel-1]
elif chr(c) == 'M':
if self.last_mark >= 0:
self.marked.extend(range(self.last_mark,self.cursel+1))
effected_lines = range(self.last_mark,self.cursel)
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")
for l in HELP_LINES:
self.popup.add_line(l)
elif chr(c) == 'p':
self.show_preferences()
return
elif chr(c) == 'e':
self.__show_events()
return
self.refresh(effected_lines)