927 lines
34 KiB
Python
927 lines
34 KiB
Python
# -*- coding: utf-8 -*-
|
|
#
|
|
# Copyright (C) 2011 Nick Lanham <nick@afternight.org>
|
|
#
|
|
# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
|
|
# the additional special exception to link portions of this program with the OpenSSL library.
|
|
# See LICENSE for more details.
|
|
#
|
|
|
|
from __future__ import division
|
|
|
|
import logging
|
|
from collections import deque
|
|
|
|
import deluge.component as component
|
|
from deluge.common import FILE_PRIORITY, fdate, fsize, ftime
|
|
from deluge.ui.client import client
|
|
from deluge.ui.console import colors
|
|
from deluge.ui.console.modes import format_utils
|
|
from deluge.ui.console.modes.basemode import BaseMode
|
|
from deluge.ui.console.modes.input_popup import InputPopup
|
|
from deluge.ui.console.modes.popup import MessagePopup, SelectablePopup
|
|
from deluge.ui.console.modes.torrent_actions import ACTION, torrent_actions_popup
|
|
|
|
try:
|
|
import curses
|
|
except ImportError:
|
|
pass
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
# Big help string that gets displayed when the user hits 'h'
|
|
HELP_STR = """\
|
|
This screen shows detailed information about a torrent, and also the \
|
|
information about the individual files in the torrent.
|
|
|
|
You can navigate the file list with the Up/Down arrows and use space to \
|
|
collapse/expand the file tree.
|
|
|
|
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:
|
|
|
|
{!info!}'h'{!normal!} - Show this help
|
|
|
|
{!info!}'a'{!normal!} - Show torrent actions popup. Here you can do things like \
|
|
pause/resume, recheck, set torrent options and so on.
|
|
|
|
{!info!}'r'{!normal!} - Rename currently highlighted folder or a file. You can't \
|
|
rename multiple files at once so you need to first clear your selection \
|
|
with {!info!}'c'{!normal!}
|
|
|
|
{!info!}'m'{!normal!} - Mark or unmark a file or a folder
|
|
{!info!}'c'{!normal!} - Un-mark all files
|
|
|
|
{!info!}Space{!normal!} - Expand/Collapse currently selected folder
|
|
|
|
{!info!}Enter{!normal!} - Show priority popup in which you can set the \
|
|
download priority of selected files and folders.
|
|
|
|
{!info!}Left Arrow{!normal!} - Go back to torrent overview.
|
|
"""
|
|
|
|
|
|
class TorrentDetail(BaseMode, component.Component):
|
|
def __init__(self, alltorrentmode, torrentid, stdscr, console_config, encoding=None):
|
|
|
|
self.console_config = console_config
|
|
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", "download_location", "file_progress", "file_priorities", "message",
|
|
"total_wanted", "tracker_host", "owner"]
|
|
|
|
self.file_list = None
|
|
self.current_file = None
|
|
self.current_file_idx = 0
|
|
self.file_off = 0
|
|
self.more_to_draw = False
|
|
self.full_names = None
|
|
|
|
self.column_string = ""
|
|
self.files_sep = None
|
|
|
|
self.marked = {}
|
|
|
|
BaseMode.__init__(self, stdscr, encoding)
|
|
component.Component.__init__(self, "TorrentDetail", 1, depend=["SessionProxy"])
|
|
|
|
self.column_names = ["Filename", "Size", "Progress", "Priority"]
|
|
self.__update_columns()
|
|
|
|
component.start(["TorrentDetail"])
|
|
|
|
self._listing_start = self.rows // 2
|
|
self._listing_space = self._listing_start - self._listing_start
|
|
|
|
client.register_event_handler("TorrentFileRenamedEvent", self._on_torrentfilerenamed_event)
|
|
client.register_event_handler("TorrentFolderRenamedEvent", self._on_torrentfolderrenamed_event)
|
|
client.register_event_handler("TorrentRemovedEvent", self._on_torrentremoved_event)
|
|
|
|
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")
|
|
|
|
if state.get("files"):
|
|
self.full_names = dict([(x["index"], x["path"]) for x in state["files"]])
|
|
|
|
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 build_file_list(self, torrent_files, progress, priority):
|
|
""" Split file list from torrent state into a directory tree.
|
|
|
|
Returns:
|
|
|
|
Tuple:
|
|
A list of lists in the form:
|
|
[file/dir_name, index, size, children, expanded, progress, priority]
|
|
|
|
Dictionary:
|
|
Map of file index for fast updating of progress and priorities.
|
|
"""
|
|
|
|
file_list = []
|
|
file_dict = {}
|
|
# directory index starts from total file count.
|
|
dir_idx = len(torrent_files)
|
|
for torrent_file in torrent_files:
|
|
cur = file_list
|
|
paths = torrent_file["path"].split("/")
|
|
for path in paths:
|
|
if not cur or path != cur[-1][0]:
|
|
child_list = []
|
|
if path == paths[-1]:
|
|
file_progress = format_utils.format_progress(progress[torrent_file["index"]] * 100)
|
|
entry = [path, torrent_file["index"], torrent_file["size"], child_list,
|
|
False, file_progress, priority[torrent_file["index"]]]
|
|
file_dict[torrent_file["index"]] = entry
|
|
else:
|
|
entry = [path, dir_idx, -1, child_list, False, 0, -1]
|
|
file_dict[dir_idx] = entry
|
|
dir_idx += 1
|
|
cur.append(entry)
|
|
cur = child_list
|
|
else:
|
|
cur = cur[-1][3]
|
|
self.__build_sizes(file_list)
|
|
self.__fill_progress(file_list, progress)
|
|
return file_list, file_dict
|
|
|
|
# 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])
|
|
child_prios = [e[6] for e in f[3]]
|
|
if len(child_prios) > 1:
|
|
f[6] = -2 # mixed
|
|
else:
|
|
f[6] = child_prios.pop(0)
|
|
|
|
def __update_columns(self):
|
|
self.column_widths = [-1, 15, 15, 20]
|
|
req = sum([col_width for col_width in self.column_widths if col_width >= 0])
|
|
if req > self.cols: # can't satisfy requests, just spread out evenly
|
|
cw = 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([col_width for col_width in self.column_widths if col_width < 0])
|
|
vw = 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 _on_torrentremoved_event(self, torrent_id):
|
|
if torrent_id == self.torrentid:
|
|
self.back_to_overview()
|
|
|
|
def _on_torrentfilerenamed_event(self, torrent_id, index, new_name):
|
|
if torrent_id == self.torrentid:
|
|
self.file_dict[index][0] = new_name.split("/")[-1]
|
|
component.get("SessionProxy").get_torrent_status(
|
|
self.torrentid, self._status_keys).addCallback(self.set_state)
|
|
|
|
def _on_torrentfolderrenamed_event(self, torrent_id, old_folder, new_folder):
|
|
if torrent_id == self.torrentid:
|
|
fe = None
|
|
fl = None
|
|
for i in old_folder.strip("/").split("/"):
|
|
if not fl:
|
|
fe = fl = self.file_list
|
|
s = [files for files in fl if files[0].strip("/") == i][0]
|
|
fe = s
|
|
fl = s[3]
|
|
fe[0] = new_folder.strip("/").rpartition("/")[-1]
|
|
|
|
# self.__get_file_by_name(old_folder, self.file_list)[0] = new_folder.strip("/")
|
|
component.get("SessionProxy").get_torrent_status(
|
|
self.torrentid, self._status_keys).addCallback(self.set_state)
|
|
|
|
def draw_files(self, files, depth, off, idx):
|
|
|
|
color_selected = "blue"
|
|
color_partially_selected = "magenta"
|
|
color_highlighted = "white"
|
|
for fl in files:
|
|
# from sys import stderr
|
|
# print >> stderr, fl[6]
|
|
# 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
|
|
|
|
# default color values
|
|
fg = "white"
|
|
bg = "black"
|
|
attr = ""
|
|
|
|
if fl[6] == -2:
|
|
pass # Mixed
|
|
elif fl[6] == 0:
|
|
fg = "red" # Do Not Download
|
|
elif fl[6] == 1:
|
|
pass # Normal
|
|
elif fl[6] <= 6:
|
|
fg = "yellow" # High
|
|
elif fl[6] == 7:
|
|
fg = "green" # Highest
|
|
|
|
if idx >= self.file_off:
|
|
# set fg/bg colors based on whether the file is selected/marked or not
|
|
|
|
if fl[1] in self.marked:
|
|
bg = color_selected
|
|
if fl[3]:
|
|
if self.marked[fl[1]] < self.__get_contained_files_count(file_list=fl[3]):
|
|
bg = color_partially_selected
|
|
attr = "bold"
|
|
|
|
if idx == self.current_file_idx:
|
|
self.current_file = fl
|
|
bg = color_highlighted
|
|
if fl[1] in self.marked:
|
|
fg = color_selected
|
|
if fl[3]:
|
|
if self.marked[fl[1]] < self.__get_contained_files_count(file_list=fl[3]):
|
|
fg = color_partially_selected
|
|
else:
|
|
if fg == "white":
|
|
fg = "black"
|
|
attr = "bold"
|
|
|
|
if attr:
|
|
color_string = "{!%s,%s,%s!}" % (fg, bg, attr)
|
|
else:
|
|
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]),
|
|
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 __get_file_list_length(self, file_list=None):
|
|
"""
|
|
Counts length of the displayed file list.
|
|
"""
|
|
if file_list is None:
|
|
file_list = self.file_list
|
|
length = 0
|
|
if file_list:
|
|
for element in file_list:
|
|
length += 1
|
|
if element[3] and element[4]:
|
|
length += self.__get_file_list_length(element[3])
|
|
return length
|
|
|
|
def __get_contained_files_count(self, file_list=None, idx=None):
|
|
length = 0
|
|
if file_list is None:
|
|
file_list = self.file_list
|
|
if idx is not None:
|
|
for element in file_list:
|
|
if element[1] == idx:
|
|
return self.__get_contained_files_count(file_list=element[3])
|
|
elif element[3]:
|
|
c = self.__get_contained_files_count(file_list=element[3], idx=idx)
|
|
if c > 0:
|
|
return c
|
|
else:
|
|
for element in file_list:
|
|
length += 1
|
|
if element[3]:
|
|
length -= 1
|
|
length += self.__get_contained_files_count(element[3])
|
|
return length
|
|
|
|
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.__update_columns()
|
|
if self.popup:
|
|
self.popup.handle_resize()
|
|
|
|
self._listing_start = self.rows // 2
|
|
self.refresh()
|
|
|
|
def render_header(self, off):
|
|
status = self.torrent_state
|
|
|
|
up_color = colors.state_color["Seeding"]
|
|
down_color = colors.state_color["Downloading"]
|
|
|
|
# Name
|
|
s = "{!info!}Name: {!input!}%s" % status["name"]
|
|
self.add_string(off, s)
|
|
off += 1
|
|
|
|
# Print DL info and ETA
|
|
if status["download_payload_rate"] > 0:
|
|
s = "%sDownloading: {!input!}" % down_color
|
|
else:
|
|
s = "{!info!}Downloaded: {!input!}"
|
|
s += fsize(status["all_time_download"])
|
|
if status["progress"] != 100.0:
|
|
s += "/%s" % fsize(status["total_wanted"])
|
|
if status["download_payload_rate"] > 0:
|
|
s += " {!yellow!}@ %s%s" % (down_color, fsize(status["download_payload_rate"]))
|
|
s += "{!info!} ETA: {!input!}%s" % format_utils.format_time(status["eta"])
|
|
self.add_string(off, s)
|
|
off += 1
|
|
|
|
# Print UL info and ratio
|
|
if status["upload_payload_rate"] > 0:
|
|
s = "%sUploading: {!input!}" % up_color
|
|
else:
|
|
s = "{!info!}Uploaded: {!input!}"
|
|
s += fsize(status["total_uploaded"])
|
|
if status["upload_payload_rate"] > 0:
|
|
s += " {!yellow!}@ %s%s" % (up_color, fsize(status["upload_payload_rate"]))
|
|
ratio_str = format_utils.format_float(status["ratio"])
|
|
if ratio_str == "-":
|
|
ratio_str = "inf"
|
|
s += " {!info!}Ratio: {!input!}%s" % ratio_str
|
|
self.add_string(off, s)
|
|
off += 1
|
|
|
|
# Seed/peer info
|
|
s = "{!info!}Seeds:{!green!} %s {!input!}(%s)" % (status["num_seeds"], status["total_seeds"])
|
|
self.add_string(off, s)
|
|
off += 1
|
|
s = "{!info!}Peers:{!red!} %s {!input!}(%s)" % (status["num_peers"], status["total_peers"])
|
|
self.add_string(off, s)
|
|
off += 1
|
|
|
|
# Tracker
|
|
if status["message"] == "OK":
|
|
color = "{!green!}"
|
|
else:
|
|
color = "{!red!}"
|
|
s = "{!info!}Tracker: {!magenta!}%s{!input!} says \"%s%s{!input!}\"" % (
|
|
status["tracker_host"], color, status["message"])
|
|
self.add_string(off, s)
|
|
off += 1
|
|
|
|
# Pieces and availability
|
|
s = "{!info!}Pieces: {!yellow!}%s {!input!}x {!yellow!}%s" % (
|
|
status["num_pieces"], fsize(status["piece_length"]))
|
|
if status["distributed_copies"]:
|
|
s += " {!info!}Availability: {!input!}%s" % format_utils.format_float(status["distributed_copies"])
|
|
self.add_string(off, s)
|
|
off += 1
|
|
|
|
# Time added
|
|
s = "{!info!}Added: {!input!}%s" % fdate(status["time_added"])
|
|
self.add_string(off, s)
|
|
off += 1
|
|
|
|
# Time active
|
|
s = "{!info!}Time active: {!input!}%s" % (ftime(status["active_time"]))
|
|
if status["seeding_time"]:
|
|
s += ", {!cyan!}%s{!input!} seeding" % (ftime(status["seeding_time"]))
|
|
self.add_string(off, s)
|
|
off += 1
|
|
|
|
# Download Folder
|
|
s = "{!info!}Download Folder: {!input!}%s" % status["download_location"]
|
|
self.add_string(off, s)
|
|
off += 1
|
|
|
|
# Owner
|
|
if status["owner"]:
|
|
s = "{!info!}Owner: {!input!}%s" % status["owner"]
|
|
|
|
return off
|
|
|
|
def refresh(self, lines=None):
|
|
# show a message popup if there's anything queued
|
|
if self.popup is None and self.messages:
|
|
title, msg = self.messages.popleft()
|
|
self.popup = MessagePopup(self, title, msg)
|
|
|
|
# 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 Exception as ex:
|
|
log.debug("Exception caught: %s", ex)
|
|
|
|
off = 1
|
|
if self.torrent_state:
|
|
off = self.render_header(off)
|
|
else:
|
|
self.add_string(1, "Waiting for torrent state")
|
|
|
|
off += 1
|
|
|
|
if self.files_sep:
|
|
self.add_string(off, self.files_sep)
|
|
off += 1
|
|
|
|
self._listing_start = off
|
|
self._listing_space = self.rows - self._listing_start
|
|
|
|
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)
|
|
|
|
if component.get("ConsoleUI").screen != self:
|
|
return
|
|
|
|
self.stdscr.noutrefresh()
|
|
|
|
if self.popup:
|
|
self.popup.refresh()
|
|
|
|
curses.doupdate()
|
|
|
|
def expcol_cur_file(self):
|
|
"""
|
|
Expand or collapse current file
|
|
"""
|
|
self.current_file[4] = not self.current_file[4]
|
|
self.refresh()
|
|
|
|
def file_list_down(self, rows=1):
|
|
maxlen = self.__get_file_list_length() - 1
|
|
|
|
self.current_file_idx += rows
|
|
|
|
if self.current_file_idx > maxlen:
|
|
self.current_file_idx = maxlen
|
|
|
|
if self.current_file_idx > self.file_off + (self._listing_space - 3):
|
|
self.file_off = self.current_file_idx - (self._listing_space - 3)
|
|
|
|
self.refresh()
|
|
|
|
def file_list_up(self, rows=1):
|
|
self.current_file_idx = max(0, self.current_file_idx - rows)
|
|
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.erase()
|
|
component.get("ConsoleUI").set_mode(self.alltorrentmode)
|
|
self.alltorrentmode._go_top = False
|
|
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:
|
|
# Do not set priorities for the whole dir, just selected contents
|
|
if f[3]:
|
|
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, was_empty):
|
|
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 was_empty:
|
|
self.marked = {}
|
|
return True
|
|
|
|
# show popup for priority selections
|
|
def show_priority_popup(self, was_empty):
|
|
def popup_func(idx, data, we=was_empty):
|
|
return self.do_priority(idx, data, we)
|
|
|
|
if self.marked:
|
|
self.popup = SelectablePopup(self, "Set File Priority", popup_func)
|
|
self.popup.add_line("_Do Not Download", data=FILE_PRIORITY["Do Not Download"], foreground="red")
|
|
self.popup.add_line("_Normal Priority", data=FILE_PRIORITY["Normal Priority"])
|
|
self.popup.add_line("_High Priority", data=FILE_PRIORITY["High Priority"], foreground="yellow")
|
|
self.popup.add_line("H_ighest Priority", data=FILE_PRIORITY["Highest Priority"], foreground="green")
|
|
self.popup._selected = 1
|
|
|
|
def __mark_unmark(self, idx):
|
|
"""
|
|
Selects or unselects file or a catalog(along with contained files)
|
|
"""
|
|
fc = self.__get_contained_files_count(idx=idx)
|
|
if idx not in self.marked:
|
|
# Not selected, select it
|
|
self.__mark_tree(self.file_list, idx)
|
|
elif self.marked[idx] < fc:
|
|
# Partially selected, unselect all contents
|
|
self.__unmark_tree(self.file_list, idx)
|
|
else:
|
|
# Selected, unselect it
|
|
self.__unmark_tree(self.file_list, idx)
|
|
|
|
def __mark_tree(self, file_list, idx, mark_all=False):
|
|
"""
|
|
Given file_list of TorrentDetail and index of file or folder,
|
|
recursively selects all files contained
|
|
as well as marks folders higher in hierarchy as partially selected
|
|
"""
|
|
total_marked = 0
|
|
for element in file_list:
|
|
marked = 0
|
|
# Select the file if it's the one we want or
|
|
# if it's inside a directory that got selected
|
|
if (element[1] == idx) or mark_all:
|
|
# If it's a folder then select everything inside
|
|
if element[3]:
|
|
marked = self.__mark_tree(element[3], idx, True)
|
|
self.marked[element[1]] = marked
|
|
else:
|
|
marked = 1
|
|
self.marked[element[1]] = 1
|
|
else:
|
|
# Does not match but the item to be selected might be inside, recurse
|
|
if element[3]:
|
|
marked = self.__mark_tree(element[3], idx, False)
|
|
# Partially select the folder if it contains files that were selected
|
|
if marked > 0:
|
|
self.marked[element[1]] = marked
|
|
else:
|
|
if element[1] in self.marked:
|
|
# It's not the element we want but it's marked so count it
|
|
marked = 1
|
|
# Count and then return total amount of files selected in all subdirectories
|
|
total_marked += marked
|
|
|
|
return total_marked
|
|
|
|
def __get_file_by_num(self, num, file_list, idx=0):
|
|
for element in file_list:
|
|
if idx == num:
|
|
return element
|
|
|
|
if element[3] and element[4]:
|
|
i = self.__get_file_by_num(num, element[3], idx + 1)
|
|
if not isinstance(i, int):
|
|
return i
|
|
else:
|
|
idx = i
|
|
else:
|
|
idx += 1
|
|
|
|
return idx
|
|
|
|
def __get_file_by_name(self, name, file_list, idx=0):
|
|
for element in file_list:
|
|
if element[0].strip("/") == name.strip("/"):
|
|
return element
|
|
|
|
if element[3] and element[4]:
|
|
i = self.__get_file_by_name(name, element[3], idx + 1)
|
|
if not isinstance(i, int):
|
|
return i
|
|
else:
|
|
idx = i
|
|
else:
|
|
idx += 1
|
|
|
|
return idx
|
|
|
|
def __unmark_tree(self, file_list, idx, unmark_all=False):
|
|
"""
|
|
Given file_list of TorrentDetail and index of file or folder,
|
|
recursively deselects all files contained
|
|
as well as marks folders higher in hierarchy as unselected or partially selected
|
|
"""
|
|
total_marked = 0
|
|
for element in file_list:
|
|
marked = 0
|
|
# It's either the item we want to select or
|
|
# a contained item, deselect it
|
|
if (element[1] == idx) or unmark_all:
|
|
if element[1] in self.marked:
|
|
del self.marked[element[1]]
|
|
# Deselect all contents if it's a catalog
|
|
if element[3]:
|
|
self.__unmark_tree(element[3], idx, True)
|
|
else:
|
|
# Not file we wanted but it might be inside this folder, recurse inside
|
|
if element[3]:
|
|
marked = self.__unmark_tree(element[3], idx, False)
|
|
# If none of the contents remain selected, unselect this folder as well
|
|
if marked == 0:
|
|
if element[1] in self.marked:
|
|
del self.marked[element[1]]
|
|
# Otherwise update selection count
|
|
else:
|
|
self.marked[element[1]] = marked
|
|
else:
|
|
if element[1] in self.marked:
|
|
marked = 1
|
|
|
|
# Count and then return selection count so we can update
|
|
# directories higher up in the hierarchy
|
|
total_marked += marked
|
|
return total_marked
|
|
|
|
def _selection_to_file_idx(self, file_list=None, idx=0, true_idx=0, closed=False):
|
|
if not file_list:
|
|
file_list = self.file_list
|
|
|
|
for element in file_list:
|
|
if idx == self.current_file_idx:
|
|
return true_idx
|
|
|
|
# It's a folder
|
|
if element[3]:
|
|
i = self._selection_to_file_idx(element[3], idx + 1, true_idx, closed or not element[4])
|
|
if isinstance(i, tuple):
|
|
idx, true_idx = i
|
|
if element[4]:
|
|
idx, true_idx = i
|
|
else:
|
|
idx += 1
|
|
_, true_idx = i
|
|
else:
|
|
return i
|
|
else:
|
|
if not closed:
|
|
idx += 1
|
|
true_idx += 1
|
|
|
|
return (idx, true_idx)
|
|
|
|
def _get_full_folder_path(self, num, file_list=None, path="", idx=0):
|
|
if not file_list:
|
|
file_list = self.file_list
|
|
|
|
for element in file_list:
|
|
if not element[3]:
|
|
idx += 1
|
|
continue
|
|
|
|
if num == idx:
|
|
return "%s%s/" % (path, element[0])
|
|
|
|
if element[4]:
|
|
i = self._get_full_folder_path(num, element[3], path + element[0] + "/", idx + 1)
|
|
if not isinstance(i, int):
|
|
return i
|
|
else:
|
|
idx = i
|
|
else:
|
|
idx += 1
|
|
|
|
return idx
|
|
|
|
def _do_rename_folder(self, torrent_id, folder, new_folder):
|
|
client.core.rename_folder(torrent_id, folder, new_folder)
|
|
|
|
def _do_rename_file(self, torrent_id, file_idx, new_filename):
|
|
if not new_filename:
|
|
return
|
|
client.core.rename_files(torrent_id, [(file_idx, new_filename)])
|
|
|
|
def _show_rename_popup(self):
|
|
# Perhaps in the future: Renaming multiple files
|
|
if self.marked:
|
|
title = "Error (Enter to close)"
|
|
text = "Sorry, you can't rename multiple files, please clear selection with {!info!}'c'{!normal!} key"
|
|
self.popup = MessagePopup(self, title, text)
|
|
else:
|
|
_file = self.__get_file_by_num(self.current_file_idx, self.file_list)
|
|
old_filename = _file[0]
|
|
|
|
idx = self._selection_to_file_idx()
|
|
tid = self.torrentid
|
|
|
|
if _file[3]:
|
|
|
|
def do_rename(result):
|
|
if not result["new_foldername"]:
|
|
return
|
|
old_fname = self._get_full_folder_path(self.current_file_idx)
|
|
new_fname = "%s/%s/" % (old_fname.strip("/").rpartition("/")[0], result["new_foldername"])
|
|
self._do_rename_folder(tid, old_fname, new_fname)
|
|
|
|
popup = InputPopup(self, "Rename folder (Esc to cancel)", close_cb=do_rename)
|
|
popup.add_text("{!info!}Renaming folder:{!input!}")
|
|
popup.add_text(" * %s\n" % old_filename)
|
|
popup.add_text_input("Enter new folder name:", "new_foldername", old_filename.strip("/"))
|
|
|
|
self.popup = popup
|
|
else:
|
|
|
|
def do_rename(result):
|
|
fname = "%s/%s" % (self.full_names[idx].rpartition("/")[0], result["new_filename"])
|
|
self._do_rename_file(tid, idx, fname)
|
|
|
|
popup = InputPopup(self, "Rename file (Esc to cancel)", close_cb=do_rename)
|
|
popup.add_text("{!info!}Renaming file:{!input!}")
|
|
popup.add_text(" * %s\n" % old_filename)
|
|
popup.add_text_input("Enter new filename:", "new_filename", old_filename)
|
|
|
|
self.popup = popup
|
|
|
|
def read_input(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 make 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:
|
|
self.file_list_up(self._listing_space - 2)
|
|
elif c == curses.KEY_HOME:
|
|
self.file_off = 0
|
|
self.current_file_idx = 0
|
|
elif c == curses.KEY_DOWN:
|
|
self.file_list_down()
|
|
elif c == curses.KEY_NPAGE:
|
|
self.file_list_down(self._listing_space - 2)
|
|
elif c == curses.KEY_END:
|
|
self.current_file_idx = self.__get_file_list_length() - 1
|
|
self.file_off = self.current_file_idx - (self._listing_space - 3)
|
|
elif c == curses.KEY_DC:
|
|
torrent_actions_popup(self, [self.torrentid], action=ACTION.REMOVE)
|
|
# Enter Key
|
|
elif c == curses.KEY_ENTER or c == 10:
|
|
was_empty = (self.marked == {})
|
|
self.__mark_tree(self.file_list, self.current_file[1])
|
|
self.show_priority_popup(was_empty)
|
|
|
|
# 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) == "r":
|
|
self._show_rename_popup()
|
|
elif chr(c) == "c":
|
|
self.marked = {}
|
|
elif chr(c) == "a":
|
|
torrent_actions_popup(self, [self.torrentid], details=False)
|
|
return
|
|
elif chr(c) == "o":
|
|
torrent_actions_popup(self, [self.torrentid], action=ACTION.TORRENT_OPTIONS)
|
|
return
|
|
elif chr(c) == "h":
|
|
self.popup = MessagePopup(self, "Help", HELP_STR, width_req=0.75)
|
|
elif chr(c) == "j":
|
|
self.file_list_up()
|
|
if chr(c) == "k":
|
|
self.file_list_down()
|
|
|
|
self.refresh()
|