From 2e2851dc13d73352530dd4495c7e05603b2e520d Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Wed, 10 Apr 2024 23:38:38 +0200 Subject: Adding upstream version 2.1.2~dev0+20240219. Signed-off-by: Daniel Baumann --- deluge/ui/gtk3/peers_tab.py | 382 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 382 insertions(+) create mode 100644 deluge/ui/gtk3/peers_tab.py (limited to 'deluge/ui/gtk3/peers_tab.py') diff --git a/deluge/ui/gtk3/peers_tab.py b/deluge/ui/gtk3/peers_tab.py new file mode 100644 index 0000000..5768fbe --- /dev/null +++ b/deluge/ui/gtk3/peers_tab.py @@ -0,0 +1,382 @@ +# +# Copyright (C) 2008 Andrew Resch +# +# 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 os.path + +from gi.repository.GdkPixbuf import Pixbuf +from gi.repository.Gtk import ( + Builder, + CellRendererPixbuf, + CellRendererProgress, + CellRendererText, + ListStore, + TreeViewColumn, + TreeViewColumnSizing, +) + +import deluge.common +import deluge.component as component +from deluge.ui.client import client +from deluge.ui.countries import COUNTRIES + +from .common import ( + icon_downloading, + icon_seeding, + load_pickled_state_file, + parse_ip_port, + save_pickled_state_file, +) +from .torrentdetails import Tab +from .torrentview_data_funcs import ( + cell_data_peer_progress, + cell_data_speed_down, + cell_data_speed_up, +) + +log = logging.getLogger(__name__) + + +class PeersTab(Tab): + def __init__(self): + super().__init__('Peers', 'peers_tab', 'peers_tab_label') + + self.peer_menu = self.main_builder.get_object('menu_peer_tab') + component.get('MainWindow').connect_signals(self) + + self.listview = self.main_builder.get_object('peers_listview') + self.listview.props.has_tooltip = True + self.listview.connect('button-press-event', self._on_button_press_event) + self.listview.connect('query-tooltip', self._on_query_tooltip) + + # flag, ip, client, downspd, upspd, country code, int_ip, seed/peer icon, progress + self.liststore = ListStore( + Pixbuf, str, str, int, int, str, float, Pixbuf, float + ) + self.cached_flag_pixbufs = {} + + self.seed_pixbuf = icon_seeding + self.peer_pixbuf = icon_downloading + + # key is ip address, item is row iter + self.peers = {} + + # Country column + column = TreeViewColumn() + render = CellRendererPixbuf() + column.pack_start(render, False) + column.add_attribute(render, 'pixbuf', 0) + column.set_sort_column_id(5) + column.set_clickable(True) + column.set_resizable(True) + column.set_expand(False) + column.set_min_width(20) + column.set_reorderable(True) + self.listview.append_column(column) + + # Address column + column = TreeViewColumn(_('Address')) + render = CellRendererPixbuf() + column.pack_start(render, False) + column.add_attribute(render, 'pixbuf', 7) + render = CellRendererText() + column.pack_start(render, False) + column.add_attribute(render, 'text', 1) + column.set_sort_column_id(6) + column.set_clickable(True) + column.set_resizable(True) + column.set_expand(False) + column.set_min_width(100) + column.set_reorderable(True) + self.listview.append_column(column) + + # Client column + column = TreeViewColumn(_('Client')) + render = CellRendererText() + column.pack_start(render, False) + column.add_attribute(render, 'text', 2) + column.set_sort_column_id(2) + column.set_clickable(True) + column.set_resizable(True) + column.set_expand(False) + column.set_min_width(100) + column.set_reorderable(True) + self.listview.append_column(column) + + # Progress column + column = TreeViewColumn(_('Progress')) + render = CellRendererProgress() + column.pack_start(render, True) + column.set_cell_data_func(render, cell_data_peer_progress, 8) + column.set_sort_column_id(8) + column.set_clickable(True) + column.set_resizable(True) + column.set_expand(False) + column.set_min_width(100) + column.set_reorderable(True) + self.listview.append_column(column) + + # Down Speed column + column = TreeViewColumn(_('Down Speed')) + render = CellRendererText() + column.pack_start(render, False) + column.set_cell_data_func(render, cell_data_speed_down, 3) + column.set_sort_column_id(3) + column.set_clickable(True) + column.set_resizable(True) + column.set_expand(False) + column.set_min_width(50) + column.set_reorderable(True) + self.listview.append_column(column) + + # Up Speed column + column = TreeViewColumn(_('Up Speed')) + render = CellRendererText() + column.pack_start(render, False) + column.set_cell_data_func(render, cell_data_speed_up, 4) + column.set_sort_column_id(4) + column.set_clickable(True) + column.set_resizable(True) + column.set_expand(False) + column.set_min_width(50) + # Bugfix: Last column needs max_width set to stop scrollbar appearing + column.set_max_width(150) + column.set_reorderable(True) + self.listview.append_column(column) + + self.listview.set_model(self.liststore) + + self.load_state() + + self.torrent_id = None + + def save_state(self): + # Get the current sort order of the view + column_id, sort_order = self.liststore.get_sort_column_id() + + # Setup state dict + state = { + 'columns': {}, + 'sort_id': column_id, + 'sort_order': int(sort_order) if sort_order else None, + } + + for index, column in enumerate(self.listview.get_columns()): + state['columns'][column.get_title()] = { + 'position': index, + 'width': column.get_width(), + } + save_pickled_state_file('peers_tab.state', state) + + def load_state(self): + state = load_pickled_state_file('peers_tab.state') + + if state is None: + return + + if len(state['columns']) != len(self.listview.get_columns()): + log.warning('peers_tab.state is not compatible! rejecting..') + return + + if state['sort_id'] and state['sort_order'] is not None: + self.liststore.set_sort_column_id(state['sort_id'], state['sort_order']) + + for index, column in enumerate(self.listview.get_columns()): + cname = column.get_title() + if cname in state['columns']: + cstate = state['columns'][cname] + column.set_sizing(TreeViewColumnSizing.FIXED) + column.set_fixed_width(cstate['width'] if cstate['width'] > 0 else 10) + if state['sort_id'] == index and state['sort_order'] is not None: + column.set_sort_indicator(True) + column.set_sort_order(state['sort_order']) + if cstate['position'] != index: + # Column is in wrong position + if cstate['position'] == 0: + self.listview.move_column_after(column, None) + elif ( + self.listview.get_columns()[cstate['position'] - 1].get_title() + != cname + ): + self.listview.move_column_after( + column, self.listview.get_columns()[cstate['position'] - 1] + ) + + def update(self): + # Get the first selected torrent + torrent_id = component.get('TorrentView').get_selected_torrents() + + # Only use the first torrent in the list or return if None selected + if len(torrent_id) != 0: + torrent_id = torrent_id[0] + else: + # No torrent is selected in the torrentview + self.liststore.clear() + return + + if torrent_id != self.torrent_id: + # We only want to do this if the torrent_id has changed + self.liststore.clear() + self.peers = {} + self.torrent_id = torrent_id + + component.get('SessionProxy').get_torrent_status( + torrent_id, ['peers'] + ).addCallback(self._on_get_torrent_status) + + def get_flag_pixbuf(self, country): + if not country.strip(): + return None + + if country not in self.cached_flag_pixbufs: + # We haven't created a pixbuf for this country yet + try: + self.cached_flag_pixbufs[country] = Pixbuf.new_from_file( + deluge.common.resource_filename( + 'deluge', + os.path.join( + 'ui', 'data', 'pixmaps', 'flags', country.lower() + '.png' + ), + ) + ) + except Exception as ex: + log.debug('Unable to load flag: %s', ex) + return None + + return self.cached_flag_pixbufs[country] + + def _on_get_torrent_status(self, status): + new_ips = set() + for peer in status['peers']: + new_ips.add(peer['ip']) + if peer['ip'] in self.peers: + # We already have this peer in our list, so lets just update it + row = self.peers[peer['ip']] + if not self.liststore.iter_is_valid(row): + # This iter is invalid, delete it and continue to next iteration + del self.peers[peer['ip']] + continue + values = self.liststore.get(row, 3, 4, 5, 7, 8) + if peer['down_speed'] != values[0]: + self.liststore.set_value(row, 3, peer['down_speed']) + if peer['up_speed'] != values[1]: + self.liststore.set_value(row, 4, peer['up_speed']) + if peer['country'] != values[2]: + self.liststore.set_value(row, 5, peer['country']) + self.liststore.set_value( + row, 0, self.get_flag_pixbuf(peer['country']) + ) + if peer['seed']: + icon = self.seed_pixbuf + else: + icon = self.peer_pixbuf + + if icon != values[3]: + self.liststore.set_value(row, 7, icon) + + if peer['progress'] != values[4]: + self.liststore.set_value(row, 8, peer['progress']) + else: + # Peer is not in list so we need to add it + + # Create an int IP address for sorting purposes + if peer['ip'].count(':') == 1: + # This is an IPv4 address + ip_int = sum( + int(byte) << shift + for byte, shift in zip( + peer['ip'].split(':')[0].split('.'), (24, 16, 8, 0) + ) + ) + peer_ip = peer['ip'] + else: + # This is an IPv6 address + import binascii + import socket + + # Split out the :port + ip = ':'.join(peer['ip'].split(':')[:-1]) + ip_int = int( + binascii.hexlify(socket.inet_pton(socket.AF_INET6, ip)), 16 + ) + peer_ip = '[{}]:{}'.format(ip, peer['ip'].split(':')[-1]) + + if peer['seed']: + icon = self.seed_pixbuf + else: + icon = self.peer_pixbuf + + row = self.liststore.append( + [ + self.get_flag_pixbuf(peer['country']), + peer_ip, + peer['client'], + peer['down_speed'], + peer['up_speed'], + peer['country'], + float(ip_int), + icon, + peer['progress'], + ] + ) + + self.peers[peer['ip']] = row + + # Now we need to remove any ips that were not in status["peers"] list + for ip in set(self.peers).difference(new_ips): + self.liststore.remove(self.peers[ip]) + del self.peers[ip] + + def clear(self): + self.liststore.clear() + + def _on_button_press_event(self, widget, event): + """This is a callback for showing the right-click context menu.""" + log.debug('on_button_press_event') + # We only care about right-clicks + if self.torrent_id and event.button == 3: + self.peer_menu.popup(None, None, None, None, event.button, event.time) + return True + + def _on_query_tooltip(self, widget, x, y, keyboard_tip, tooltip): + is_tooltip, x, y, model, path, _iter = widget.get_tooltip_context( + x, y, keyboard_tip + ) + if is_tooltip: + country_code = model.get(_iter, 5)[0] + if country_code != ' ' and country_code in COUNTRIES: + tooltip.set_text(COUNTRIES[country_code]) + # widget here is self.listview + widget.set_tooltip_cell(tooltip, path, widget.get_column(0), None) + return True + return False + + def on_menuitem_add_peer_activate(self, menuitem): + """This is a callback for manually adding a peer""" + log.debug('on_menuitem_add_peer') + builder = Builder() + builder.add_from_file( + deluge.common.resource_filename( + __package__, os.path.join('glade', 'connect_peer_dialog.ui') + ) + ) + peer_dialog = builder.get_object('connect_peer_dialog') + txt_ip = builder.get_object('txt_ip') + response = peer_dialog.run() + + if response: + value = txt_ip.get_text() + ip, port = parse_ip_port(value) + if ip and port: + log.info('Adding peer IP: %s port: %s to %s', ip, port, self.torrent_id) + client.core.connect_peer(self.torrent_id, ip, port) + else: + log.error('Error parsing peer "%s"', value) + + peer_dialog.destroy() + return True -- cgit v1.2.3