def on_tribler_statistics(self, data): if not data: return data = data["tribler_statistics"] self.window().general_tree_widget.clear() self.create_and_add_widget_item("Tribler version", self.tribler_version, self.window().general_tree_widget) self.create_and_add_widget_item( "Python version", sys.version.replace('\n', ''), self.window().general_tree_widget # to fit in one line ) self.create_and_add_widget_item("Libtorrent version", libtorrent.version, self.window().general_tree_widget) self.create_and_add_widget_item("", "", self.window().general_tree_widget) self.create_and_add_widget_item("Number of channels", data["num_channels"], self.window().general_tree_widget) self.create_and_add_widget_item( "Database size", format_size(data["db_size"]), self.window().general_tree_widget ) self.create_and_add_widget_item( "Number of known torrents", data["num_torrents"], self.window().general_tree_widget ) self.create_and_add_widget_item("", "", self.window().general_tree_widget) disk_usage = psutil.disk_usage('/') self.create_and_add_widget_item( "Total disk space", format_size(disk_usage.total), self.window().general_tree_widget ) self.create_and_add_widget_item( "Used disk space", format_size(disk_usage.used), self.window().general_tree_widget ) self.create_and_add_widget_item( "Free disk space", format_size(disk_usage.free), self.window().general_tree_widget )
def update_torrent_size_label(self): total_files_size = self.dialog_widget.files_list_view.total_files_size selected_files_size = self.dialog_widget.files_list_view.selected_files_size if total_files_size == selected_files_size: label_text = tr("Torrent size: ") + format_size(total_files_size) else: label_text = (tr("Selected: ") + format_size(selected_files_size) + " / " + tr("Total: ") + format_size(total_files_size)) self.dialog_widget.loading_files_label.setStyleSheet("color:#ffffff;") self.dialog_widget.loading_files_label.setText(label_text)
def should_cleanup_old_versions(self) -> List[TriblerVersion]: if self.version_history.last_run_version == self.version_history.code_version: return [] disposable_versions = self.version_history.get_disposable_versions(skip_versions=2) if not disposable_versions: return [] storage_info = "" claimable_storage = 0 for version in disposable_versions: state_size = version.calc_state_size() claimable_storage += state_size storage_info += f"{version.version_str} \t {format_size(state_size)}\n" # Show a question to the user asking if the user wants to remove the old data. title = "Delete state directories for old versions?" message_body = tr( "Press 'Yes' to remove state directories for older versions of Tribler " "and reclaim %s of storage space. " "Tribler used those directories during upgrades from previous versions. " "Now those directories can be safely deleted. \n\n" "If unsure, press 'No'. " "You will be able to remove those directories from the Settings->Data page later." ) % format_size(claimable_storage) user_choice = self._show_question_box(title, message_body, storage_info, default_button=QMessageBox.Yes) if user_choice == QMessageBox.Yes: return disposable_versions return []
def update_item(self): self.setText(0, self.file_info["name"]) self.setText(1, format_size(float(self.file_info["size"]))) self.setText( 2, '{percent:.1%}'.format(percent=self.file_info["progress"])) self.setText(3, "yes" if self.file_info["included"] else "no") self.setData(0, Qt.UserRole, self.file_info)
def fill_directory_sizes(self): if self.file_size is None: self.file_size = 0 for child in self.children: self.file_size += child.fill_directory_sizes() self.setText(SIZE_COL, format_size(float(self.file_size))) return self.file_size
def update_item(self): self.setText(0, self.download_info["name"]) if self.download_info["size"] == 0 and self.get_raw_download_status() == DLSTATUS_METADATA: self.setText(1, "unknown") else: self.setText(1, format_size(float(self.download_info["size"]))) try: self.progress_slider.setValue(int(self.download_info["progress"] * 100)) except RuntimeError: self._logger.error("The underlying GUI widget has already been removed.") if self.download_info["vod_mode"]: self.setText(3, "Streaming") else: self.setText(3, DLSTATUS_STRINGS[dlstatus_strings.index(self.download_info["status"])]) self.setText(4, "%s (%s)" % (self.download_info["num_connected_seeds"], self.download_info["num_seeds"])) self.setText(5, "%s (%s)" % (self.download_info["num_connected_peers"], self.download_info["num_peers"])) self.setText(6, format_speed(self.download_info["speed_down"])) self.setText(7, format_speed(self.download_info["speed_up"])) self.setText(8, "%.3f" % float(self.download_info["ratio"])) self.setText(9, "yes" if self.download_info["anon_download"] else "no") self.setText(10, str(self.download_info["hops"]) if self.download_info["anon_download"] else "-") self.setText(12, datetime.fromtimestamp(int(self.download_info["time_added"])).strftime('%Y-%m-%d %H:%M')) eta_text = "-" if self.get_raw_download_status() == DLSTATUS_DOWNLOADING: eta_text = duration_to_string(self.download_info["eta"]) self.setText(11, eta_text)
def define_columns(): d = ColumnDefinition # fmt:off # pylint: disable=line-too-long columns_dict = { Column.ACTIONS: d('', "", width=60, sortable=False), Column.CATEGORY: d('category', "", width=30, tooltip_filter=lambda data: data), Column.NAME: d('name', tr("Name"), width=EXPANDING), Column.SIZE: d('size', tr("Size"), width=90, display_filter=lambda data: (format_size(float(data)) if data != "" else "")), Column.HEALTH: d( 'health', tr("Health"), width=100, tooltip_filter=lambda data: f"{data}" + ('' if data == HEALTH_CHECKING else '\n(Click to recheck)'), ), Column.UPDATED: d( 'updated', tr("Updated"), width=120, display_filter=lambda timestamp: pretty_date(timestamp) if timestamp and timestamp > BITTORRENT_BIRTHDAY else 'N/A', ), Column.VOTES: d( 'votes', tr("Popularity"), width=120, display_filter=format_votes, tooltip_filter=lambda data: get_votes_rating_description(data) if data is not None else None, ), Column.STATUS: d('status', "", sortable=False), Column.STATE: d('state', "", width=80, tooltip_filter=lambda data: data, sortable=False), Column.TORRENTS: d('torrents', tr("Torrents"), width=90), Column.SUBSCRIBED: d('subscribed', tr("Subscribed"), width=90), } # pylint: enable=line-too-long # fmt:on return columns_dict
def on_received_metainfo(self, response): if not response or not self or self.closed: return if 'error' in response: if response['error'] == 'metainfo error': # If it failed to load metainfo for max number of times, show an error message in red. if self.metainfo_retries > METAINFO_MAX_RETRIES: self.dialog_widget.loading_files_label.setStyleSheet( "color:#ff0000;") self.dialog_widget.loading_files_label.setText( "Failed to load files. Click to retry again.") return self.perform_files_request() elif 'code' in response['error'] and response['error'][ 'code'] == 'IOError': self.dialog_widget.loading_files_label.setText( "Unable to read torrent file data") else: self.dialog_widget.loading_files_label.setText( f"Error: {response['error']}") return metainfo = json.loads(unhexlify(response['metainfo'])) if 'files' in metainfo['info']: # Multi-file torrent files = metainfo['info']['files'] else: files = [{ 'path': [metainfo['info']['name']], 'length': metainfo['info']['length'] }] # Show if the torrent already exists in the downloads if response.get('download_exists'): self.dialog_widget.existing_download_info_label.setText( "Note: this torrent already exists in the Downloads") else: self.dialog_widget.existing_download_info_label.setText("") self.dialog_widget.files_list_view.clear() for filename in files: item = DownloadFileTreeWidgetItem( self.dialog_widget.files_list_view) item.setText(0, '/'.join(filename['path'])) item.setText(1, format_size(float(filename['length']))) item.setData(0, Qt.UserRole, filename) item.setCheckState(2, Qt.Checked) self.dialog_widget.files_list_view.addTopLevelItem(item) self.has_metainfo = True self.dialog_widget.loading_files_label.setHidden(True) self.dialog_widget.download_files_container.setHidden(False) self.dialog_widget.files_list_view.setHidden(False) self.dialog_widget.adjustSize() self.on_main_window_resize() self.received_metainfo.emit(metainfo)
def update_pages(self, new_download=False): if self.current_download is None: return if "files" not in self.current_download: self.current_download["files"] = [] self.window().download_progress_bar.update_with_download(self.current_download) self.window().download_detail_name_label.setText(self.current_download['name']) if self.current_download["vod_mode"]: self.window().download_detail_status_label.setText('Streaming') else: status_string = DLSTATUS_STRINGS[dlstatus_strings.index(self.current_download["status"])] if dlstatus_strings.index(self.current_download["status"]) == DLSTATUS_STOPPED_ON_ERROR: status_string += f" (error: {self.current_download['error']})" self.window().download_detail_status_label.setText(status_string) self.window().download_detail_filesize_label.setText( tr("%(num_bytes)s in %(num_files)d files") % { 'num_bytes': format_size(float(self.current_download["size"])), 'num_files': len(self.current_download["files"]), } ) self.window().download_detail_health_label.setText( tr("%d seeders, %d leechers") % (self.current_download["num_seeds"], self.current_download["num_peers"]) ) self.window().download_detail_infohash_label.setText(self.current_download['infohash']) self.window().download_detail_destination_label.setText(self.current_download["destination"]) self.window().download_detail_ratio_label.setText( "%.3f, up: %s, down: %s" % ( self.current_download["ratio"], format_size(self.current_download["total_up"]), format_size(self.current_download["total_down"]), ) ) self.window().download_detail_availability_label.setText(f"{self.current_download['availability']:.2f}") if force_update := (new_download or self.window().download_files_list.is_empty): # (re)populate the files list self.window().download_files_list.clear() files = convert_to_files_tree_format(self.current_download) self.window().download_files_list.fill_entries(files)
def update_status_bar(self, selected_node): if not selected_node: return peer_message = f"<b>User</b> {HTML_SPACE * 16}{selected_node.get('public_key', '')[:74]}..." self.window().tr_selected_node_pub_key.setHidden(False) self.window().tr_selected_node_pub_key.setText(peer_message) diff = selected_node.get('total_up', 0) - selected_node.get( 'total_down', 0) color = COLOR_GREEN if diff > 0 else COLOR_RED if diff < 0 else COLOR_DEFAULT bandwidth_message = ( "<b>Bandwidth</b> " + HTML_SPACE * 2 + " Given " + HTML_SPACE + html_label(format_size(selected_node.get('total_up', 0))) + " Taken " + HTML_SPACE + html_label(format_size(selected_node.get('total_down', 0))) + " Balance " + HTML_SPACE + html_label(format_size(diff), color=color)) self.window().tr_selected_node_stats.setHidden(False) self.window().tr_selected_node_stats.setText(bandwidth_message)
def add_items_to_tree(self, tree, items, keys): tree.clear() for item in items: widget_item = QTreeWidgetItem(tree) for index, key in enumerate(keys): if key in ["bytes_up", "bytes_down"]: value = format_size(item[key]) elif key in ["creation_time", "last_lookup"]: value = str(datetime.timedelta(seconds=int(time() - item[key]))) if item[key] > 0 else '-' else: value = str(item[key]) widget_item.setText(index, value) tree.addTopLevelItem(widget_item)
def update_with_torrent(self, index, torrent_info): self.torrent_info = torrent_info self.index = index self.torrent_detail_name_label.setText(self.torrent_info["name"]) if self.torrent_info["category"]: self.torrent_detail_category_label.setText( self.torrent_info["category"].lower()) else: self.torrent_detail_category_label.setText("unknown") if self.torrent_info["size"] is None: self.torrent_detail_size_label.setText("Size: -") else: self.torrent_detail_size_label.setText( "%s" % format_size(float(self.torrent_info["size"]))) self.update_health_label(torrent_info['num_seeders'], torrent_info['num_leechers'], torrent_info['last_tracker_check']) self.torrent_detail_infohash_label.setText( self.torrent_info["infohash"]) self.setCurrentIndex(0) self.setTabEnabled(1, True) # If we do not have the health of this torrent, query it, but do it delayed. # When the user scrolls the list, we only want to trigger health checks on the line # that the user stopped on, so we do not generate excessive health checks. if self.is_health_checking: if self.rest_request1: self.rest_request1.cancel_request() if self.rest_request2: self.rest_request2.cancel_request() self.is_health_checking = False if torrent_info['last_tracker_check'] == 0: if not self.healthcheck_timer.isActive(): self.on_check_health_clicked() self.healthcheck_timer.stop() self.healthcheck_timer.start(HEALTHCHECK_DELAY) self.update_health_label(torrent_info['num_seeders'], torrent_info['num_leechers'], torrent_info['last_tracker_check']) self.torrent_detail_trackers_list.clear()
def fill_entries(self, files): if not files: return # Block the signals to prevent unnecessary recalculation of directory sizes self.blockSignals(True) self.clear() # ACHTUNG! # Workaround for QT eliding size text too aggressively, resulting in incorrect column size # The downside is no eliding for the names column self.setTextElideMode(Qt.ElideNone) self.header().setSectionResizeMode(QHeaderView.ResizeToContents) single_item_torrent = len(files) == 1 # !!! ACHTUNG !!! # The styling must be applied right before or right after filling the table, # otherwise it won't work properly. self.setStyleSheet(TORRENT_FILES_TREE_STYLESHEET) self.total_files_size = 0 items = {'': self} for file_index, file in enumerate(files): path = file['path'] for i, obj_name in enumerate(path): parent_path = "/".join(path[:i]) full_path = "/".join(path[:i + 1]) if full_path in items: continue is_file = i == len(path) - 1 item = items[full_path] = DownloadFileTreeWidgetItem( items[parent_path], file_index=file_index if is_file else None, file_progress=file.get('progress'), ) item.setText(FILENAME_COL, obj_name) item.setData(FILENAME_COL, Qt.UserRole, obj_name) file_included = file.get('included', True) item.setCheckState( CHECKBOX_COL, Qt.Checked if file_included else Qt.Unchecked) if single_item_torrent: item.setFlags(item.flags() & ~Qt.ItemIsUserCheckable) if is_file: # Add file size info for file entries item.file_size = int(file['length']) self.total_files_size += item.file_size item.setText(SIZE_COL, format_size(float(file['length']))) else: # Make folder checkboxes automatically affect subtree items item.setFlags(item.flags() | Qt.ItemIsAutoTristate) for ind in range(self.topLevelItemCount()): self.topLevelItem(ind).fill_directory_sizes() # Automatically open the toplevel item if self.topLevelItemCount() == 1: item = self.topLevelItem(0) if item.childCount() > 0: self.expandItem(item) self.blockSignals(False) self.selected_files_size = sum(item.file_size for item in self.get_selected_items() if item.file_index is not None) self.selected_files_changed.emit()
def update_pages(self, new_download=False): if self.current_download is None: return if "files" not in self.current_download: self.current_download["files"] = [] self.window().download_progress_bar.update_with_download( self.current_download) self.window().download_detail_name_label.setText( self.current_download['name']) if self.current_download["vod_mode"]: self.window().download_detail_status_label.setText('Streaming') else: status_string = DLSTATUS_STRINGS[dlstatus_strings.index( self.current_download["status"])] if dlstatus_strings.index(self.current_download["status"] ) == DLSTATUS_STOPPED_ON_ERROR: status_string += f" (error: {self.current_download['error']})" self.window().download_detail_status_label.setText(status_string) self.window().download_detail_filesize_label.setText( tr("%(num_bytes)s in %(num_files)d files") % { 'num_bytes': format_size(float(self.current_download["size"])), 'num_files': len(self.current_download["files"]), }) self.window().download_detail_health_label.setText( tr("%d seeders, %d leechers") % (self.current_download["num_seeds"], self.current_download["num_peers"])) self.window().download_detail_infohash_label.setText( self.current_download['infohash']) self.window().download_detail_destination_label.setText( self.current_download["destination"]) self.window().download_detail_ratio_label.setText( "%.3f, up: %s, down: %s" % ( self.current_download["ratio"], format_size(self.current_download["total_up"]), format_size(self.current_download["total_down"]), )) self.window().download_detail_availability_label.setText( f"{self.current_download['availability']:.2f}") if new_download or len(self.current_download["files"]) != len( self.files_widgets.keys()): # (re)populate the files list self.window().download_files_list.clear() self.files_widgets = {} for dfile in self.current_download["files"]: item = DownloadFileWidgetItem( self.window().download_files_list, dfile) DownloadsDetailsTabWidget.update_file_row(item, dfile) self.files_widgets[dfile["name"]] = item else: # No new download, just update data in the lists for dfile in self.current_download["files"]: DownloadsDetailsTabWidget.update_file_row( self.files_widgets[dfile["name"]], dfile) # Populate the trackers list self.window().download_trackers_list.clear() for tracker in self.current_download["trackers"]: item = QTreeWidgetItem(self.window().download_trackers_list) DownloadsDetailsTabWidget.update_tracker_row(item, tracker) # Populate the peers list if the peer information is available self.window().download_peers_list.clear() if "peers" in self.current_download: for peer in self.current_download["peers"]: item = QTreeWidgetItem(self.window().download_peers_list) DownloadsDetailsTabWidget.update_peer_row(item, peer)
class ChannelContentModel(RemoteTableModel): columns = [ u'category', u'name', u'size', u'health', u'updated', ACTION_BUTTONS ] column_headers = [ u'Category', u'Name', u'Size', u'Health', u'Updated', u'' ] unsortable_columns = [u'status', u'state', ACTION_BUTTONS] column_flags = { u'subscribed': Qt.ItemIsEnabled | Qt.ItemIsSelectable, u'category': Qt.ItemIsEnabled | Qt.ItemIsSelectable, u'name': Qt.ItemIsEnabled | Qt.ItemIsSelectable, u'torrents': Qt.ItemIsEnabled | Qt.ItemIsSelectable, u'size': Qt.ItemIsEnabled | Qt.ItemIsSelectable, u'updated': Qt.ItemIsEnabled | Qt.ItemIsSelectable, u'health': Qt.ItemIsEnabled | Qt.ItemIsSelectable, u'votes': Qt.ItemIsEnabled | Qt.ItemIsSelectable, u'state': Qt.ItemIsEnabled | Qt.ItemIsSelectable, u'status': Qt.ItemIsEnabled | Qt.ItemIsSelectable, ACTION_BUTTONS: Qt.ItemIsEnabled | Qt.ItemIsSelectable, } column_width = { u'state': lambda _: 20, u'name': lambda table_width: table_width - 510, u'action_buttons': lambda _: 70, } column_tooltip_filters = { u'state': lambda data: data, u'votes': lambda data: "{0:.0%}".format(float(data)) if data else None, } column_display_filters = { u'size': lambda data: (format_size(float(data)) if data != '' else ''), u'votes': format_votes, u'state': lambda data: str(data)[:1] if data == u'Downloading' else "", u'updated': lambda timestamp: pretty_date(timestamp) if timestamp and timestamp > BITTORRENT_BIRTHDAY else 'N/A', } def __init__( self, channel_info=None, hide_xxx=None, exclude_deleted=None, subscribed_only=None, endpoint_url=None, text_filter='', ): RemoteTableModel.__init__(self, parent=None) self.column_position = {name: i for i, name in enumerate(self.columns)} self.data_items = [] # Remote query (model) parameters self.hide_xxx = hide_xxx self.text_filter = text_filter self.subscribed_only = subscribed_only self.exclude_deleted = exclude_deleted self.type_filter = None self.category_filter = None # Current channel attributes. This is intentionally NOT copied, so local changes # can propagate to the origin, e.g. parent channel. self.channel_info = channel_info or { "name": "My channels", "status": 123 } self.endpoint_url_override = endpoint_url # Load the initial batch of entries self.perform_query() @property def edit_enabled(self): return False @property def endpoint_url(self): return self.endpoint_url_override or "channels/%s/%i" % ( self.channel_info["public_key"], self.channel_info["id"], ) def headerData(self, num, orientation, role=None): if orientation == Qt.Horizontal and role == Qt.DisplayRole: return self.column_headers[num] if role == Qt.InitialSortOrderRole and num != self.column_position.get( 'name'): return Qt.DescendingOrder def rowCount(self, parent=QModelIndex()): return len(self.data_items) def columnCount(self, parent=QModelIndex()): return len(self.columns) def flags(self, index): return self.column_flags[self.columns[index.column()]] def filter_item_txt(self, txt_filter, index, show_default=True): # FIXME: dumb workaround for some mysterious race condition try: item = self.data_items[index.row()] except IndexError: return "" column = self.columns[index.column()] data = item.get(column, u'') # Print number of torrents in the channel for channel rows in the "size" column if (column == "size" and "torrents" not in self.columns and "torrents" in item and item["type"] in (CHANNEL_TORRENT, COLLECTION_NODE)): return item["torrents"] if column in txt_filter: display_txt = txt_filter.get(column, str(data))(data) elif show_default: display_txt = data else: display_txt = None return display_txt def data(self, index, role): if role == Qt.DisplayRole or role == Qt.EditRole: return self.filter_item_txt(self.column_display_filters, index) elif role == Qt.ToolTipRole: return self.filter_item_txt(self.column_tooltip_filters, index, show_default=False) elif role == Qt.TextAlignmentRole: if index.column() == self.column_position.get(u'votes', -1): return Qt.AlignLeft | Qt.AlignVCenter return None def reset(self): self.item_uid_map.clear() super(ChannelContentModel, self).reset() def update_node_info(self, update_dict): """ This method updates/inserts rows based on updated_dict. It should be typically invoked by a signal from Events endpoint. One special case it when the channel_info of the model itself is updated. In that case, info_changed signal is emitted, so the controller/widget knows it is time to update the labels. """ # TODO: better mechanism for identifying channel entries for pushing updates if (self.channel_info.get("public_key") == update_dict.get("public_key") is not None and self.channel_info.get("id") == update_dict.get("id") is not None): self.channel_info.update(**update_dict) self.info_changed.emit([]) row = self.item_uid_map.get(get_item_uid(update_dict)) if row in self.data_items: self.data_items[row].update(**update_dict) self.dataChanged.emit(self.index(row, 0), self.index(row, len(self.columns)), []) def perform_query(self, **kwargs): """ Fetch search results. """ if self.type_filter is not None: kwargs.update({"metadata_type": self.type_filter}) if self.subscribed_only is not None: kwargs.update({"subscribed": self.subscribed_only}) if self.exclude_deleted is not None: kwargs.update({"exclude_deleted": self.exclude_deleted}) if self.category_filter is not None: if self.category_filter == "Channels": kwargs.update({'metadata_type': 'channel'}) else: kwargs.update({"category": self.category_filter}) if "total" not in self.channel_info: # Only include total for the first query to the endpoint kwargs.update({"include_total": 1}) super(ChannelContentModel, self).perform_query(**kwargs) def setData(self, index, new_value, role=None): if role != Qt.EditRole: return True item = self.data_items[index.row()] attribute_name = self.columns[index.column()] attribute_name = u'tags' if attribute_name == u'category' else attribute_name attribute_name = u'title' if attribute_name == u'name' else attribute_name attribute_name = u'subscribed' if attribute_name == u'votes' else attribute_name def on_row_update_results(response): if not response: return item_row = self.item_uid_map.get(get_item_uid(item)) if item_row is None: return data_item_dict = index.model().data_items[item_row] data_item_dict.update(response) self.info_changed.emit([data_item_dict]) TriblerNetworkRequest( f"metadata/{item['public_key']}/{item['id']}", on_row_update_results, method='PATCH', raw_data=json.dumps({attribute_name: new_value}), ) # TODO: reload the whole row from DB instead of just changing the displayed value self.data_items[index.row()][self.columns[index.column()]] = new_value return True def on_new_entry_received(self, response): self.on_query_results(response, remote=True)
class ChannelContentModel(RemoteTableModel): columns = [ACTION_BUTTONS, 'category', 'name', 'size', 'health', 'updated'] column_headers = [ '', '', tr('Name'), tr('Size'), tr('Health'), tr('Updated') ] unsortable_columns = ['status', 'state', ACTION_BUTTONS] column_flags = { 'subscribed': Qt.ItemIsEnabled | Qt.ItemIsSelectable, 'category': Qt.ItemIsEnabled | Qt.ItemIsSelectable, 'name': Qt.ItemIsEnabled | Qt.ItemIsSelectable, 'torrents': Qt.ItemIsEnabled | Qt.ItemIsSelectable, 'size': Qt.ItemIsEnabled | Qt.ItemIsSelectable, 'updated': Qt.ItemIsEnabled | Qt.ItemIsSelectable, 'health': Qt.ItemIsEnabled | Qt.ItemIsSelectable, 'votes': Qt.ItemIsEnabled | Qt.ItemIsSelectable, 'state': Qt.ItemIsEnabled | Qt.ItemIsSelectable, 'status': Qt.ItemIsEnabled | Qt.ItemIsSelectable, ACTION_BUTTONS: Qt.ItemIsEnabled | Qt.ItemIsSelectable, } column_width = { 'state': lambda _: 100, 'subscribed': lambda _: 100, 'name': lambda table_width: table_width - 450, 'action_buttons': lambda _: 50, 'category': lambda _: 30, } column_tooltip_filters = { 'state': lambda data: data, 'votes': lambda data: get_votes_rating_description(data) if data is not None else None, 'category': lambda data: data, 'health': lambda data: f"{data}" + ('' if data == HEALTH_CHECKING else '\n(Click to recheck)'), } column_display_filters = { 'size': lambda data: (format_size(float(data)) if data != '' else ''), 'votes': format_votes, 'updated': lambda timestamp: pretty_date(timestamp) if timestamp and timestamp > BITTORRENT_BIRTHDAY else 'N/A', } def __init__( self, channel_info=None, hide_xxx=None, exclude_deleted=None, subscribed_only=None, endpoint_url=None, text_filter='', type_filter=None, ): RemoteTableModel.__init__(self, parent=None) self.column_position = {name: i for i, name in enumerate(self.columns)} # Remote query (model) parameters self.hide_xxx = hide_xxx self.text_filter = text_filter self.subscribed_only = subscribed_only self.exclude_deleted = exclude_deleted self.type_filter = type_filter self.category_filter = None # Current channel attributes. This is intentionally NOT copied, so local changes # can propagate to the origin, e.g. parent channel. self.channel_info = channel_info or { "name": "My channels", "status": 123 } self.endpoint_url_override = endpoint_url # Load the initial batch of entries self.perform_query() @property def edit_enabled(self): return False @property def endpoint_url(self): return self.endpoint_url_override or "channels/%s/%i" % ( self.channel_info["public_key"], self.channel_info["id"], ) def headerData(self, num, orientation, role=None): if orientation == Qt.Horizontal and role == Qt.DisplayRole: return self.column_headers[num] if role == Qt.InitialSortOrderRole and num != self.column_position.get( 'name'): return Qt.DescendingOrder if role == Qt.TextAlignmentRole: return (Qt.AlignHCenter if num in [ self.column_position.get('subscribed'), self.column_position.get('torrents') ] else Qt.AlignLeft) return super().headerData(num, orientation, role) def rowCount(self, *_, **__): return len(self.data_items) def columnCount(self, *_, **__): return len(self.columns) def flags(self, index): return self.column_flags[self.columns[index.column()]] def filter_item_txt(self, txt_filter, index, show_default=True): # ACHTUNG! Dumb workaround for some mysterious race condition try: item = self.data_items[index.row()] except IndexError: return "" column = self.columns[index.column()] data = item.get(column, '') # Print number of torrents in the channel for channel rows in the "size" column if (column == "size" and "torrents" not in self.columns and "torrents" in item and item["type"] in (CHANNEL_TORRENT, COLLECTION_NODE)): return item["torrents"] # 'subscribed' column gets special treatment in case of ToolTipRole, because # its tooltip uses information from both 'subscribed' and 'state' keys if (column == 'subscribed' and txt_filter == self.column_tooltip_filters and 'subscribed' in item and 'state' in item): state_message = f" ({item['state']})" if item[ 'state'] != CHANNEL_STATE.COMPLETE.value else "" tooltip_txt = ( f"Subscribed.{state_message}\n(Click to unsubscribe)" if item['subscribed'] else "Not subscribed.\n(Click to subscribe)") return tooltip_txt if column in txt_filter: display_txt = txt_filter.get(column, str(data))(data) elif show_default: display_txt = data else: display_txt = None return display_txt def data(self, index, role): if role in (Qt.DisplayRole, Qt.EditRole): return self.filter_item_txt(self.column_display_filters, index) if role == Qt.ToolTipRole: return self.filter_item_txt(self.column_tooltip_filters, index, show_default=False) if role == Qt.TextAlignmentRole: if index.column() == self.column_position.get('votes', -1): return Qt.AlignLeft | Qt.AlignVCenter if index.column() == self.column_position.get('torrents', -1): return Qt.AlignHCenter | Qt.AlignVCenter return None def reset(self): self.item_uid_map.clear() super().reset() def update_node_info(self, update_dict): """ This method updates/inserts rows based on updated_dict. It should be typically invoked by a signal from Events endpoint. One special case it when the channel_info of the model itself is updated. In that case, info_changed signal is emitted, so the controller/widget knows it is time to update the labels. """ if (self.channel_info.get("public_key") == update_dict.get("public_key") is not None and self.channel_info.get("id") == update_dict.get("id") is not None): self.channel_info.update(**update_dict) self.info_changed.emit([]) return row = self.item_uid_map.get(get_item_uid(update_dict)) if row is not None and row < len(self.data_items): self.data_items[row].update(**update_dict) self.dataChanged.emit(self.index(row, 0), self.index(row, len(self.columns)), []) def perform_query(self, **kwargs): """ Fetch search results. """ if self.type_filter is not None: kwargs.update({"metadata_type": self.type_filter}) else: kwargs.update( {"metadata_type": [REGULAR_TORRENT, COLLECTION_NODE]}) if self.subscribed_only is not None: kwargs.update({"subscribed": self.subscribed_only}) if self.exclude_deleted is not None: kwargs.update({"exclude_deleted": self.exclude_deleted}) if self.category_filter is not None: if self.category_filter == "Channels": kwargs.update({'metadata_type': 'channel'}) else: kwargs.update({"category": self.category_filter}) if "total" not in self.channel_info: # Only include total for the first query to the endpoint kwargs.update({"include_total": 1}) super().perform_query(**kwargs) def setData(self, index, new_value, role=None): if role != Qt.EditRole: return True item = self.data_items[index.row()] attribute_name = self.columns[index.column()] attribute_name = 'tags' if attribute_name == 'category' else attribute_name attribute_name = 'title' if attribute_name == 'name' else attribute_name if attribute_name == 'subscribed': return True def on_row_update_results(response): if not response: return item_row = self.item_uid_map.get(get_item_uid(item)) if item_row is None: return data_item_dict = index.model().data_items[item_row] data_item_dict.update(response) self.info_changed.emit([data_item_dict]) TriblerNetworkRequest( f"metadata/{item['public_key']}/{item['id']}", on_row_update_results, method='PATCH', raw_data=json.dumps({attribute_name: new_value}), ) # ACHTUNG: instead of reloading the whole row from DB, this line just changes the displayed value! self.data_items[index.row()][self.columns[index.column()]] = new_value return True def on_new_entry_received(self, response): self.on_query_results(response, remote=True)
def update_item(self): self.setText(0, self.file_info["name"]) self.setText(1, format_size(float(self.file_info["size"]))) self.setText(2, f"{self.file_info['progress']:.1%}") self.setText(3, "yes" if self.file_info["included"] else "no") self.setData(0, Qt.UserRole, self.file_info)
def tickStrings(self, values, scale, spacing): return [format_size(value, precision=3) for value in values]