summaryrefslogtreecommitdiffstats
path: root/deluge/ui/console/modes/torrentdetail.py
diff options
context:
space:
mode:
Diffstat (limited to 'deluge/ui/console/modes/torrentdetail.py')
-rw-r--r--deluge/ui/console/modes/torrentdetail.py1021
1 files changed, 1021 insertions, 0 deletions
diff --git a/deluge/ui/console/modes/torrentdetail.py b/deluge/ui/console/modes/torrentdetail.py
new file mode 100644
index 0000000..4383d58
--- /dev/null
+++ b/deluge/ui/console/modes/torrentdetail.py
@@ -0,0 +1,1021 @@
+#
+# 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.
+#
+
+import logging
+
+import deluge.component as component
+from deluge.common import fsize
+from deluge.decorators import overrides
+from deluge.ui.client import client
+from deluge.ui.common import FILE_PRIORITY
+from deluge.ui.console.modes.basemode import BaseMode
+from deluge.ui.console.modes.torrentlist.torrentactions import (
+ ACTION,
+ torrent_actions_popup,
+)
+from deluge.ui.console.utils import colors
+from deluge.ui.console.utils import curses_util as util
+from deluge.ui.console.utils.column import get_column_value, torrent_data_fields
+from deluge.ui.console.utils.format_utils import (
+ format_priority,
+ format_progress,
+ format_row,
+)
+from deluge.ui.console.widgets.popup import (
+ InputPopup,
+ MessagePopup,
+ PopupsHandler,
+ SelectablePopup,
+)
+
+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, PopupsHandler):
+ def __init__(self, parent_mode, stdscr, console_config, encoding=None):
+ PopupsHandler.__init__(self)
+ self.console_config = console_config
+ self.parent_mode = parent_mode
+ self.torrentid = None
+ self.torrent_state = None
+ 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',
+ 'seed_rank',
+ 'last_seen_complete',
+ 'completed_time',
+ 'time_since_transfer',
+ 'super_seeding',
+ ]
+ 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)
+ self.column_names = ['Filename', 'Size', 'Progress', 'Priority']
+ self.__update_columns()
+
+ 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
+ )
+
+ util.safe_curs_set(util.Curser.INVISIBLE)
+ self.stdscr.notimeout(0)
+
+ def set_torrent_id(self, torrentid):
+ self.torrentid = torrentid
+ self.file_list = None
+
+ def back_to_overview(self):
+ component.get('ConsoleUI').set_mode(self.parent_mode.mode_name)
+
+ @overrides(component.Component)
+ def start(self):
+ self.update()
+
+ @overrides(component.Component)
+ def update(self, torrentid=None):
+ if torrentid:
+ self.set_torrent_id(torrentid)
+
+ if self.torrentid:
+ component.get('SessionProxy').get_torrent_status(
+ self.torrentid, self._status_keys
+ ).addCallback(self.set_state)
+
+ @overrides(BaseMode)
+ def pause(self):
+ self.set_torrent_id(None)
+
+ @overrides(BaseMode)
+ def on_resize(self, rows, cols):
+ BaseMode.on_resize(self, rows, cols)
+ self.__update_columns()
+ if self.popup:
+ self.popup.handle_resize()
+
+ self._listing_start = self.rows // 2
+ self.refresh()
+
+ def set_state(self, state):
+ if state.get('files'):
+ self.full_names = {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']
+ )
+ 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 and self.file_list:
+ 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_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_progress(bd // f[2] * 100)
+ else: # file, update own prog and add to total
+ bd = f[2] * progs[f[1]]
+ f[5] = 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 _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 = ''
+
+ priority_fg_color = {
+ -2: 'white', # Mixed
+ 0: 'red', # Skip
+ 1: 'yellow', # Low
+ 2: 'yellow',
+ 3: 'yellow',
+ 4: 'white', # Normal
+ 5: 'green',
+ 6: 'green',
+ 7: 'green', # High
+ }
+
+ fg = priority_fg_color[fl[6]]
+
+ 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 = f'{{!{fg},{bg},{attr}!}}'
+ else:
+ color_string = f'{{!{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_row(
+ [
+ '{}{} {}'.format(' ' * depth, xchar, fl[0]),
+ fsize(fl[2]),
+ fl[5],
+ format_priority(fl[6]),
+ ],
+ self.column_widths,
+ )
+
+ self.add_string(off, f'{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 render_header(self, row):
+ status = self.torrent_state
+
+ download_color = '{!info!}'
+ if status['download_payload_rate'] > 0:
+ download_color = colors.state_color['Downloading']
+
+ def add_field(name, row, pre_color='{!info!}', post_color='{!input!}'):
+ s = '{}{}: {}{}'.format(
+ pre_color,
+ torrent_data_fields[name]['name'],
+ post_color,
+ get_column_value(name, status),
+ )
+ if row:
+ row = self.add_string(row, s)
+ return row
+ return s
+
+ # Name
+ row = add_field('name', row)
+ # State
+ row = add_field('state', row)
+
+ # Print DL info and ETA
+ s = add_field('downloaded', 0, download_color)
+ if status['progress'] != 100.0:
+ s += '/%s' % fsize(status['total_wanted'])
+ if status['download_payload_rate'] > 0:
+ s += ' {{!yellow!}}@ {}{}'.format(
+ download_color,
+ fsize(status['download_payload_rate']),
+ )
+ s += add_field('eta', 0)
+ if s:
+ row = self.add_string(row, s)
+
+ # Print UL info and ratio
+ s = add_field('uploaded', 0, download_color)
+ if status['upload_payload_rate'] > 0:
+ s += ' {{!yellow!}}@ {}{}'.format(
+ colors.state_color['Seeding'],
+ fsize(status['upload_payload_rate']),
+ )
+ s += ' ' + add_field('ratio', 0)
+ row = self.add_string(row, s)
+
+ # Seed/peer info
+ s = '{{!info!}}{}:{{!green!}} {} {{!input!}}({})'.format(
+ torrent_data_fields['seeds']['name'],
+ status['num_seeds'],
+ status['total_seeds'],
+ )
+ row = self.add_string(row, s)
+ s = '{{!info!}}{}:{{!red!}} {} {{!input!}}({})'.format(
+ torrent_data_fields['peers']['name'],
+ status['num_peers'],
+ status['total_peers'],
+ )
+ row = self.add_string(row, s)
+
+ # Tracker
+ tracker_color = '{!green!}' if status['message'] == 'OK' else '{!red!}'
+ s = '{{!info!}}{}: {{!magenta!}}{}{{!input!}} says "{}{}{{!input!}}"'.format(
+ torrent_data_fields['tracker']['name'],
+ status['tracker_host'],
+ tracker_color,
+ status['message'],
+ )
+ row = self.add_string(row, s)
+
+ # Pieces and availability
+ s = '{{!info!}}{}: {{!yellow!}}{} {{!input!}}x {{!yellow!}}{}'.format(
+ torrent_data_fields['pieces']['name'],
+ status['num_pieces'],
+ fsize(status['piece_length']),
+ )
+ if status['distributed_copies']:
+ s += '{{!info!}}{}: {{!input!}}{}'.format(
+ torrent_data_fields['seed_rank']['name'],
+ status['seed_rank'],
+ )
+ row = self.add_string(row, s)
+
+ # Time added
+ row = add_field('time_added', row)
+ # Time active
+ row = add_field('active_time', row)
+ if status['seeding_time']:
+ row = add_field('seeding_time', row)
+ # Download Folder
+ row = add_field('download_location', row)
+ # Seed Rank
+ row = add_field('seed_rank', row)
+ # Super Seeding
+ row = add_field('super_seeding', row)
+ # Last seen complete
+ row = add_field('last_seen_complete', row)
+ # Last activity
+ row = add_field('time_since_transfer', row)
+ # Owner
+ if status['owner']:
+ row = add_field('owner', row)
+ return row
+ # Last act
+
+ @overrides(BaseMode)
+ def refresh(self, lines=None):
+ # Update the status bars
+ self.stdscr.erase()
+ self.draw_statusbars()
+
+ row = 1
+ if self.torrent_state:
+ row = self.render_header(row)
+ else:
+ self.add_string(1, 'Waiting for torrent state')
+
+ row += 1
+
+ if self.files_sep:
+ self.add_string(row, self.files_sep)
+ row += 1
+
+ self._listing_start = row
+ self._listing_space = self.rows - self._listing_start
+
+ self.add_string(row, self.column_string)
+ if self.file_list:
+ row += 1
+ self.more_to_draw = False
+ self.draw_files(self.file_list, 0, row, 0)
+
+ if not component.get('ConsoleUI').is_active_mode(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()
+
+ # 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, priority, was_empty):
+ plist = []
+ self.build_prio_list(self.file_list, plist, -1, priority)
+ plist.sort()
+ priorities = [p[1] for p in plist]
+ client.core.set_torrent_options(
+ [self.torrentid], {'file_priorities': priorities}
+ )
+
+ if was_empty:
+ self.marked = {}
+ return True
+
+ # show popup for priority selections
+ def show_priority_popup(self, was_empty):
+ def popup_func(name, data, was_empty, **kwargs):
+ if not name:
+ return
+ return self.do_priority(data[name], was_empty)
+
+ if self.marked:
+ popup = SelectablePopup(
+ self,
+ 'Set File Priority',
+ popup_func,
+ border_off_north=1,
+ cb_args={'was_empty': was_empty},
+ )
+ popup.add_line(
+ 'skip_priority',
+ '_Skip',
+ foreground='red',
+ cb_arg=FILE_PRIORITY['Skip'],
+ was_empty=was_empty,
+ )
+ popup.add_line(
+ 'low_priority', '_Low', cb_arg=FILE_PRIORITY['Low'], foreground='yellow'
+ )
+ popup.add_line('normal_priority', '_Normal', cb_arg=FILE_PRIORITY['Normal'])
+ popup.add_line(
+ 'high_priority',
+ '_High',
+ cb_arg=FILE_PRIORITY['High'],
+ foreground='green',
+ )
+ popup._selected = 1
+ self.push_popup(popup)
+
+ 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
+ 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
+ tmp, 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 f'{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
+ 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:
+ self.report_message(
+ 'Error (Enter to close)',
+ 'Sorry, you cannot rename multiple files, please clear '
+ 'selection with {!info!}"c"{!normal!} key',
+ )
+ 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, **kwargs):
+ if (
+ not result
+ or not result['new_foldername']['value']
+ or kwargs.get('close', False)
+ ):
+ self.popup.close(None, call_cb=False)
+ return
+ old_fname = self._get_full_folder_path(self.current_file_idx)
+ new_fname = '{}/{}/'.format(
+ old_fname.strip('/').rpartition('/')[0],
+ result['new_foldername']['value'],
+ )
+ self._do_rename_folder(tid, old_fname, new_fname)
+
+ popup = InputPopup(
+ self, 'Rename folder (Esc to cancel)', close_cb=do_rename
+ )
+ popup.add_text_input(
+ 'new_foldername',
+ 'Enter new folder name:',
+ old_filename.strip('/'),
+ complete=True,
+ )
+ self.push_popup(popup)
+ else:
+
+ def do_rename(result, **kwargs):
+ if (
+ not result
+ or not result['new_filename']['value']
+ or kwargs.get('close', False)
+ ):
+ self.popup.close(None, call_cb=False)
+ return
+ fname = '{}/{}'.format(
+ self.full_names[idx].rpartition('/')[0],
+ result['new_filename']['value'],
+ )
+ self._do_rename_file(tid, idx, fname)
+
+ popup = InputPopup(self, ' Rename file ', close_cb=do_rename)
+ popup.add_text_input(
+ 'new_filename', 'Enter new filename:', old_filename, complete=True
+ )
+ self.push_popup(popup)
+
+ @overrides(BaseMode)
+ def read_input(self):
+ c = self.stdscr.getch()
+
+ if self.popup:
+ ret = self.popup.handle_read(c)
+ if ret != util.ReadState.IGNORED and self.popup.closed():
+ self.pop_popup()
+ self.refresh()
+ return
+
+ if c in [util.KEY_ESC, curses.KEY_LEFT, ord('q')]:
+ self.back_to_overview()
+ return util.ReadState.READ
+
+ 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)
+ elif c in [curses.KEY_ENTER, util.KEY_ENTER2]:
+ was_empty = self.marked == {}
+ self.__mark_tree(self.file_list, self.current_file[1])
+ self.show_priority_popup(was_empty)
+ elif c == util.KEY_SPACE:
+ self.expcol_cur_file()
+ elif c == ord('m'):
+ if self.current_file:
+ self.__mark_unmark(self.current_file[1])
+ elif c == ord('r'):
+ self._show_rename_popup()
+ elif c == ord('c'):
+ self.marked = {}
+ elif c == ord('a'):
+ torrent_actions_popup(self, [self.torrentid], details=False)
+ return
+ elif c == ord('o'):
+ torrent_actions_popup(self, [self.torrentid], action=ACTION.TORRENT_OPTIONS)
+ return
+ elif c == ord('h'):
+ self.push_popup(MessagePopup(self, 'Help', HELP_STR, width_req=0.75))
+ elif c == ord('j'):
+ self.file_list_down()
+ elif c == ord('k'):
+ self.file_list_up()
+
+ self.refresh()