def on_add_torrents_browse_dir(self, checked): chosen_dir = QFileDialog.getExistingDirectory( self, "Please select the directory containing the .torrent files", QDir.homePath(), QFileDialog.ShowDirsOnly ) if not chosen_dir: return self.chosen_dir = chosen_dir self.dialog = ConfirmationDialog( self, "Add torrents from directory", f"Add all torrent files from the following directory to your Tribler channel:\n\n{chosen_dir}", [('ADD', BUTTON_TYPE_NORMAL), ('CANCEL', BUTTON_TYPE_CONFIRM)], checkbox_text="Include subdirectories (recursive mode)", ) connect(self.dialog.button_clicked, self.on_confirm_add_directory_dialog) self.dialog.show()
def start_download_from_uri(self, uri): uri = uri.decode('utf-8') if isinstance(uri, bytes) else uri self.download_uri = uri if get_gui_setting(self.gui_settings, "ask_download_settings", True, is_bool=True): # FIXME: instead of using this workaround, make sure the settings are _available_ by this moment # If tribler settings is not available, fetch the settings and inform the user to try again. if not self.tribler_settings: self.fetch_settings() self.dialog = ConfirmationDialog.show_error( self, tr("Download Error"), tr("Tribler settings is not available yet. Fetching it now. Please try again later."), ) # By re-adding the download uri to the pending list, the request is re-processed # when the settings is received self.pending_uri_requests.append(uri) return # Clear any previous dialog if exists if self.dialog: self.dialog.close_dialog() self.dialog = None self.dialog = StartDownloadDialog(self, self.download_uri) connect(self.dialog.button_clicked, self.on_start_download_action) self.dialog.show() self.start_download_dialog_active = True else: # FIXME: instead of using this workaround, make sure the settings are _available_ by this moment # In the unlikely scenario that tribler settings are not available yet, try to fetch settings again and # add the download uri back to self.pending_uri_requests to process again. if not self.tribler_settings: self.fetch_settings() if self.download_uri not in self.pending_uri_requests: self.pending_uri_requests.append(self.download_uri) return self.window().perform_start_download_request( self.download_uri, self.window().tribler_settings['download_defaults']['anonymity_enabled'], self.window().tribler_settings['download_defaults']['safeseeding_enabled'], self.tribler_settings['download_defaults']['saveas'], [], ) self.process_uri_request()
def on_create_clicked(self): if self.dialog_widget.create_torrent_files_list.count() == 0: dialog = ConfirmationDialog( self.dialog_widget, "Warning!", "You should add at least one file to your torrent.", [('CLOSE', BUTTON_TYPE_NORMAL)], ) dialog.button_clicked.connect(dialog.close_dialog) dialog.show() return self.dialog_widget.btn_create.setEnabled(False) files_list = [] for ind in range(self.dialog_widget.create_torrent_files_list.count()): file_str = self.dialog_widget.create_torrent_files_list.item( ind).text() files_list.append(file_str) export_dir = self.dialog_widget.file_export_dir.text() if not os.path.exists(export_dir): ConfirmationDialog.show_error( self.dialog_widget, "Cannot save torrent file to %s" % export_dir, "Path does not exist") return is_writable, error = is_dir_writable(export_dir) if not is_writable: ConfirmationDialog.show_error( self.dialog_widget, "Cannot save torrent file to %s" % export_dir, "Error: %s" % error) return self.name = self.dialog_widget.create_torrent_name_field.text() description = self.dialog_widget.create_torrent_description_field.toPlainText( ) post_data = { "name": self.name, "description": description, "files": files_list, "export_dir": export_dir } url = ("createtorrent?download=1" if self.dialog_widget.seed_after_adding_checkbox.isChecked() else "createtorrent") self.rest_request1 = TriblerNetworkRequest(url, self.on_torrent_created, data=post_data, method='POST') self.dialog_widget.edit_channel_create_torrent_progress_label.setText( "Creating torrent. Please wait...")
def on_low_storage(self): """ Dealing with low storage space available. First stop the downloads and the core manager and ask user to user to make free space. :return: """ self.downloads_page.stop_loading_downloads() self.core_manager.shutting_down = True self.core_manager.stop(False) close_dialog = ConfirmationDialog( self.window(), "<b>CRITICAL ERROR</b>", "You are running low on disk space (<100MB). Please make sure to have " "sufficient free space available and restart Tribler again.", [("Close Tribler", BUTTON_TYPE_NORMAL)], ) close_dialog.button_clicked.connect(lambda _: self.close_tribler()) close_dialog.show()
def on_browse_dir_clicked(self, checked): chosen_dir = QFileDialog.getExistingDirectory( self.window(), "Please select the destination directory of your download", "", QFileDialog.ShowDirsOnly) if len(chosen_dir) != 0: self.dialog_widget.destination_input.setCurrentText(chosen_dir) is_writable, error = is_dir_writable(chosen_dir) if not is_writable: gui_error_message = ( "Tribler cannot download to <i>%s</i> directory. Please add proper write " "permissions to the directory or choose another download directory. [%s]" % (chosen_dir, error)) ConfirmationDialog.show_message(self.dialog_widget, "Insufficient Permissions", gui_error_message, "OK")
def on_add_torrent_from_url(self, checked=False): # Make sure that the window is visible (this action might be triggered from the tray icon) self.raise_window() if not self.add_torrent_url_dialog_active: self.dialog = ConfirmationDialog( self, "Add torrent from URL/magnet link", "Please enter the URL/magnet link in the field below:", [('ADD', BUTTON_TYPE_NORMAL), ('CANCEL', BUTTON_TYPE_CONFIRM)], show_input=True, ) self.dialog.dialog_widget.dialog_input.setPlaceholderText( 'URL/magnet link') self.dialog.dialog_widget.dialog_input.setFocus() connect(self.dialog.button_clicked, self.on_torrent_from_url_dialog_done) self.dialog.show() self.add_torrent_url_dialog_active = True
def on_new_version_available(self, version): if version == str(self.gui_settings.value('last_reported_version')): return # To prevent multiple dialogs on top of each other, # close any existing dialog first. if self.new_version_dialog: self.new_version_dialog.close_dialog() self.new_version_dialog = None self.new_version_dialog = ConfirmationDialog( self, "New version available", "Version %s of Tribler is available.Do you want to visit the " "website to download the newest version?" % version, [('IGNORE', BUTTON_TYPE_NORMAL), ('LATER', BUTTON_TYPE_NORMAL), ('OK', BUTTON_TYPE_NORMAL)], ) connect(self.new_version_dialog.button_clicked, lambda action: self.on_new_version_dialog_done(version, action)) self.new_version_dialog.show()
def on_download_clicked(self, checked): if self.has_metainfo and len(self.get_selected_files() ) == 0: # User deselected all torrents ConfirmationDialog.show_error( self.window(), "No files selected", "Please select at least one file to download.") else: download_dir = self.dialog_widget.destination_input.currentText() is_writable, error = is_dir_writable(download_dir) if not is_writable: gui_error_message = ( "Tribler cannot download to <i>%s</i> directory. Please add proper write " "permissions to the directory or choose another download directory and try " "to download again. [%s]" % (download_dir, error)) ConfirmationDialog.show_message(self.dialog_widget, "Insufficient Permissions", gui_error_message, "OK") else: self.button_clicked.emit(1)
def on_add_torrent_browse_dir(self, checked): chosen_dir = QFileDialog.getExistingDirectory( self, tr("Please select the directory containing the .torrent files"), QDir.homePath(), QFileDialog.ShowDirsOnly, ) self.chosen_dir = chosen_dir if len(chosen_dir) != 0: self.selected_torrent_files = [torrent_file for torrent_file in glob.glob(chosen_dir + "/*.torrent")] self.dialog = ConfirmationDialog( self, tr("Add torrents from directory"), tr("Add %s torrent files from the following directory to your Tribler channel: \n\n%s") % (len(self.selected_torrent_files), chosen_dir), [(tr("ADD"), BUTTON_TYPE_NORMAL), (tr("CANCEL"), BUTTON_TYPE_CONFIRM)], checkbox_text=tr("Add torrents to My Channel"), ) connect(self.dialog.button_clicked, self.on_confirm_add_directory_dialog) self.dialog.show()
def on_export_download(self, checked): self.export_dir = QFileDialog.getExistingDirectory( self, "Please select the destination directory", "", QFileDialog.ShowDirsOnly ) selected_item = self.selected_items[:1] if len(self.export_dir) > 0 and selected_item: # Show confirmation dialog where we specify the name of the file torrent_name = selected_item[0].download_info['name'] self.dialog = ConfirmationDialog( self, "Export torrent file", "Please enter the name of the torrent file:", [('SAVE', BUTTON_TYPE_NORMAL), ('CANCEL', BUTTON_TYPE_CONFIRM)], show_input=True, ) self.dialog.dialog_widget.dialog_input.setPlaceholderText('Torrent file name') self.dialog.dialog_widget.dialog_input.setText(f"{torrent_name}.torrent") self.dialog.dialog_widget.dialog_input.setFocus() connect(self.dialog.button_clicked, self.on_export_download_dialog_done) self.dialog.show()
def on_settings_saved(self, data): if not data: return # Now save the GUI settings self.window().gui_settings.setValue("family_filter", self.window().family_filter_checkbox.isChecked()) self.window().gui_settings.setValue("autocommit_enabled", self.window().channel_autocommit_checkbox.isChecked()) self.window().gui_settings.setValue( "ask_download_settings", self.window().always_ask_location_checkbox.isChecked() ) self.window().gui_settings.setValue( "use_monochrome_icon", self.window().use_monochrome_icon_checkbox.isChecked() ) self.saved_dialog = ConfirmationDialog( TriblerRequestManager.window, "Settings saved", "Your settings have been saved.", [('CLOSE', BUTTON_TYPE_NORMAL)], ) connect(self.saved_dialog.button_clicked, self.on_dialog_cancel_clicked) self.saved_dialog.show() self.window().fetch_settings()
def on_memory_dump_button_clicked(self, dump_core): self.export_dir = QFileDialog.getExistingDirectory( self, "Please select the destination directory", "", QFileDialog.ShowDirsOnly ) if len(self.export_dir) > 0: filename = "tribler_mem_dump_%s_%s.json" % ( 'core' if dump_core else 'gui', datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S"), ) if dump_core: self.rest_request = TriblerNetworkRequest( "debug/memory/dump", lambda data, _: self.on_memory_dump_data_available(filename, data) ) elif scanner: scanner.dump_all_objects(os.path.join(self.export_dir, filename)) else: ConfirmationDialog.show_error( self.window(), "Error when performing a memory dump", "meliae memory dumper is not compatible with Python 3", )
def on_emptying_tokens(self, data): if not data: return json_data = json.dumps(data) if has_qr: self.empty_tokens_barcode_dialog = QWidget() self.empty_tokens_barcode_dialog.setWindowTitle("Please scan the following QR code") self.empty_tokens_barcode_dialog.setGeometry(10, 10, 500, 500) qr = qrcode.QRCode(version=1, error_correction=qrcode.constants.ERROR_CORRECT_M, box_size=10, border=5) qr.add_data(json_data) qr.make(fit=True) img = qr.make_image() # PIL format qim = ImageQt(img) pixmap = QtGui.QPixmap.fromImage(qim).scaled(600, 600, QtCore.Qt.KeepAspectRatio) label = QLabel(self.empty_tokens_barcode_dialog) label.setPixmap(pixmap) self.empty_tokens_barcode_dialog.resize(pixmap.width(), pixmap.height()) self.empty_tokens_barcode_dialog.show() else: ConfirmationDialog.show_error(self.window(), DEPENDENCY_ERROR_TITLE, DEPENDENCY_ERROR_MESSAGE)
def on_start_download_action(self, action): if action == 1: if self.dialog and self.dialog.dialog_widget: self.window().perform_start_download_request( self.download_uri, self.dialog.dialog_widget.anon_download_checkbox.isChecked(), self.dialog.dialog_widget.safe_seed_checkbox.isChecked(), self.dialog.dialog_widget.destination_input.currentText(), self.dialog.get_selected_files(), self.dialog.dialog_widget.files_list_view.topLevelItemCount(), add_to_channel=self.dialog.dialog_widget.add_to_channel_checkbox.isChecked(), ) else: ConfirmationDialog.show_error(self, "Tribler UI Error", "Something went wrong. Please try again.") logging.exception("Error while trying to download. Either dialog or dialog.dialog_widget is None") if self.dialog: self.dialog.close_dialog() self.dialog = None self.start_download_dialog_active = False if action == 0: # We do this after removing the dialog since process_uri_request is blocking self.process_uri_request()
def show_new_order_dialog(self, is_ask): if not self.wallets[self.chosen_wallets[0]]['created']: ConfirmationDialog.show_error( self.window(), "Wallet not available", "%s wallet not available, please create it first." % self.chosen_wallets[0], ) return elif not self.wallets[self.chosen_wallets[1]]['created']: ConfirmationDialog.show_error( self.window(), "Wallet not available", "%s wallet not available, please create it first." % self.chosen_wallets[1], ) return self.dialog = NewMarketOrderDialog(self.window().stackedWidget, is_ask, self.chosen_wallets[0], self.chosen_wallets[1], self.wallets) self.dialog.button_clicked.connect(self.on_new_order_action) self.dialog.show()
def on_add_torrent_from_url(self): # Make sure that the window is visible (this action might be triggered from the tray icon) self.raise_window() if self.video_player_page.isVisible(): # If we're adding a torrent from the video player page, go to the home page. # This is necessary since VLC takes the screen and the popup becomes invisible. self.clicked_menu_button_downloads() if not self.add_torrent_url_dialog_active: self.dialog = ConfirmationDialog( self, "Add torrent from URL/magnet link", "Please enter the URL/magnet link in the field below:", [('ADD', BUTTON_TYPE_NORMAL), ('CANCEL', BUTTON_TYPE_CONFIRM)], show_input=True, ) self.dialog.dialog_widget.dialog_input.setPlaceholderText( 'URL/magnet link') self.dialog.dialog_widget.dialog_input.setFocus() self.dialog.button_clicked.connect( self.on_torrent_from_url_dialog_done) self.dialog.show() self.add_torrent_url_dialog_active = True
def confirm_partially_empty_tokens(self, action): tokens = self.empty_partial_tokens_dialog.dialog_widget.dialog_input.text() self.empty_partial_tokens_dialog.close_dialog() self.empty_partial_tokens_dialog = None if action == 0: try: tokens = int(float(tokens)) except ValueError: ConfirmationDialog.show_error(self.window(), "Wrong input", "The provided amount is not a number") return self.confirm_empty_tokens_dialog = ConfirmationDialog( self, "Empty tokens into another account", "Are you sure you want to empty %d bandwidth tokens " "into another account? " "Warning: one-way action that cannot be revered" % tokens, [('EMPTY', BUTTON_TYPE_NORMAL), ('CANCEL', BUTTON_TYPE_CONFIRM)], ) self.confirm_empty_tokens_dialog.button_clicked.connect( lambda action2: self.on_confirm_partially_empty_tokens(action2, tokens) ) self.confirm_empty_tokens_dialog.show()
def on_low_storage(self, _): """ Dealing with low storage space available. First stop the downloads and the core manager and ask user to user to make free space. :return: """ def close_tribler_gui(): self.close_tribler() # Since the core has already stopped at this point, it will not terminate the GUI. # So, we quit the GUI separately here. if not QApplication.closingDown(): QApplication.quit() self.downloads_page.stop_loading_downloads() self.core_manager.stop(False) close_dialog = ConfirmationDialog( self.window(), "<b>CRITICAL ERROR</b>", "You are running low on disk space (<100MB). Please make sure to have " "sufficient free space available and restart Tribler again.", [("Close Tribler", BUTTON_TYPE_NORMAL)], ) connect(close_dialog.button_clicked, lambda _: close_tribler_gui()) close_dialog.show()
class ChannelContentsWidget(AddBreadcrumbOnShowMixin, widget_form, widget_class): def __init__(self, parent=None): widget_class.__init__(self, parent=parent) # ACHTUNG! This is a dumb workaround for a bug(?) in PyQT bindings in Python 3.7 # When more than a single instance of a class is created, every next setupUi # triggers connectSlotsByName error. There are some reports that it is specific to # 3.7 and there is a fix in the 10.08.2019 PyQt bindings snapshot. try: self.setupUi(self) except SystemError: pass # ! ACHTUNG ! # There is a bug in PyQT bindings that prevents uic.loadUiType from correctly # detecting paths to external resources used in .ui files. Therefore, # for each external resource (e.g. image/icon), we must reload it manually here. self.channel_options_button.setIcon( QIcon(get_image_path('ellipsis.png'))) self.default_channel_model = ChannelContentModel self.initialized = False self.chosen_dir = None self.dialog = None self.controller = None self.commit_timer = None self.autocommit_enabled = None self.channel_options_menu = None self.channels_stack = [] self.categories = () self_ref = self self.hide_xxx = None # This context manager is used to freeze the state of controls in the models stack to # prevent signals from triggering for inactive models. class freeze_controls_class: objects_to_freeze = [ self_ref.category_selector, self_ref.content_table.horizontalHeader(), self_ref.channel_torrents_filter_input, ] def __enter__(self): for obj in self.objects_to_freeze: obj.blockSignals(True) def __exit__(self, *args): for obj in self.objects_to_freeze: obj.blockSignals(False) self.freeze_controls = freeze_controls_class self.channel_description_container.setHidden(True) self.explanation_tooltip_button.setHidden(True) def hide_all_labels(self): self.edit_channel_contents_top_bar.setHidden(True) self.subscription_widget.setHidden(True) self.channel_num_torrents_label.setHidden(True) self.channel_state_label.setHidden(True) @property def personal_channel_model(self): return SimplifiedPersonalChannelsModel if self.autocommit_enabled else PersonalChannelsModel @property def model(self): return self.channels_stack[-1] if self.channels_stack else None def on_channel_committed(self, response): if not response or not response.get("success", False): return if not self.autocommit_enabled: self.commit_control_bar.setHidden(True) if not self.model: return info = self.model.channel_info if info.get("state") == "Personal" and info.get("dirty"): self.model.reset() self.update_labels() def commit_channels(self, checked=False): # pylint: disable=W0613 TriblerNetworkRequest("channels/mychannel/0/commit", self.on_channel_committed, method='POST') def initialize_content_page( self, autocommit_enabled=False, hide_xxx=None, controller_class=ContentTableViewController, categories=CATEGORY_SELECTOR_FOR_SEARCH_ITEMS, ): if self.initialized: return self.hide_xxx = hide_xxx self.initialized = True self.categories = categories self.category_selector.addItems(self.categories) connect(self.category_selector.currentIndexChanged, self.on_category_selector_changed) self.channel_back_button.setIcon(QIcon( get_image_path('page_back.png'))) connect(self.channel_back_button.clicked, self.go_back) connect(self.channel_name_label.linkActivated, self.on_breadcrumb_clicked) self.commit_control_bar.setHidden(True) if LINUX: # On Linux, the default font sometimes does not contain the emoji characters. self.category_selector.setStyleSheet( "font-family: Noto Color Emoji") self.controller = controller_class( self.content_table, filter_input=self.channel_torrents_filter_input) # Hide channel description on scroll connect(self.controller.table_view.verticalScrollBar().valueChanged, self._on_table_scroll) self.autocommit_enabled = autocommit_enabled if self.autocommit_enabled: self._enable_autocommit_timer() # New channel button connect(self.new_channel_button.clicked, self.create_new_channel) connect(self.content_table.channel_clicked, self.on_channel_clicked) connect(self.edit_channel_commit_button.clicked, self.commit_channels) self.subscription_widget.initialize(self) self.channel_options_menu = self.create_channel_options_menu() self.channel_options_button.setMenu(self.channel_options_menu) connect(self.channel_description_container.became_hidden, self.run_brain_dead_refresh) connect(self.channel_description_container.description_changed, self._description_changed) def _description_changed(self): # Initialize commit timer on channel description change if self.autocommit_enabled: self.commit_timer.stop() self.commit_timer.start(CHANNEL_COMMIT_DELAY) self.model.channel_info["dirty"] = True self.update_labels() def run_brain_dead_refresh(self): if self.model: self.controller.brain_dead_refresh() def _on_table_scroll(self, event): # pylint: disable=unused-argument # Hide the description widget when the channel is scrolled down if not self.model.data_items: return scrollbar = self.controller.table_view.verticalScrollBar() container = self.channel_description_container is_time_to_hide = scrollbar.minimum( ) < scrollbar.value() - 10 and scrollbar.maximum() > 100 is_time_to_show = scrollbar.minimum() == scrollbar.value() if is_time_to_hide and not container.isHidden(): container.setHidden(True) elif is_time_to_show and container.isHidden( ) and container.initialized: container.setHidden(False) def _enable_autocommit_timer(self): self.commit_timer = QTimer() self.commit_timer.setSingleShot(True) connect(self.commit_timer.timeout, self.commit_channels) # Commit the channel just in case there are uncommitted changes left since the last time (e.g. Tribler crashed) # The timer thing here is a workaround for race condition with the core startup self.controller.table_view.setColumnHidden(3, True) self.commit_timer.stop() self.commit_timer.start(10000) def on_category_selector_changed(self, ind): category = self.categories[ind] if ind else None content_category = ContentCategories.get(category) category_code = content_category.code if content_category else category if self.model.category_filter != category_code: self.model.category_filter = category_code self.model.reset() def empty_channels_stack(self): if self.channels_stack: self.disconnect_current_model() self.channels_stack = [] def push_channels_stack(self, model): if self.model: self.model.saved_header_state = self.controller.table_view.horizontalHeader( ).saveState() self.model.saved_scroll_state = self.controller.table_view.verticalScrollBar( ).value() self.disconnect_current_model() self.channels_stack.append(model) self.connect_current_model() with self.freeze_controls(): self.category_selector.setCurrentIndex(0) self.content_table.horizontalHeader().setSortIndicator( -1, Qt.AscendingOrder) self.channel_torrents_filter_input.setText("") def on_model_info_changed(self, changed_entries): self.window().channels_menu_list.reload_if_necessary(changed_entries) dirty = False structure_changed = False for entry in changed_entries: dirty = dirty or entry.get('status', None) in DIRTY_STATUSES structure_changed = ( structure_changed or entry.get("state", None) == "Deleted" or (entry.get("type", None) in [CHANNEL_TORRENT, COLLECTION_NODE] and entry["status"] in DIRTY_STATUSES)) if structure_changed: self.window().add_to_channel_dialog.clear_channels_tree() if self.autocommit_enabled and dirty: self.commit_timer.stop() self.commit_timer.start(CHANNEL_COMMIT_DELAY) self.model.channel_info["dirty"] = dirty self.update_labels() def initialize_root_model_from_channel_info(self, channel_info): if channel_info.get("state") == CHANNEL_STATE.PERSONAL.value: self.default_channel_model = self.personal_channel_model else: self.default_channel_model = ChannelContentModel model = self.default_channel_model(hide_xxx=self.hide_xxx, channel_info=channel_info) self.initialize_root_model(model) def initialize_root_model(self, root_model): self.empty_channels_stack() self.push_channels_stack(root_model) self.controller.set_model(self.model) # Hide the edit controls by default, to prevent the user clicking the buttons prematurely self.hide_all_labels() def reset_view(self, text_filter=None, category_filter=None): if not self.model: return self.model.text_filter = text_filter or '' self.model.category_filter = category_filter with self.freeze_controls(): self._set_filter_controls_from_model() self.controller.table_view.horizontalHeader().setSortIndicator( -1, Qt.DescendingOrder) self.model.sort_by = ( self.model.columns[self.model.default_sort_column].dict_key if self.model.default_sort_column >= 0 else None) self.model.sort_desc = True self.model.reset() def disconnect_current_model(self): disconnect(self.window().core_manager.events_manager.node_info_updated, self.model.update_node_info) disconnect(self.model.info_changed, self.on_model_info_changed) self.controller.unset_model() # Disconnect the selectionChanged signal def connect_current_model(self): connect(self.model.info_changed, self.on_model_info_changed) connect(self.window().core_manager.events_manager.node_info_updated, self.model.update_node_info) @property def current_level(self): return len(self.channels_stack) - 1 def go_back(self, checked=False): # pylint: disable=W0613 self.go_back_to_level(self.current_level - 1) def on_breadcrumb_clicked(self, tgt_level): if int(tgt_level) != self.current_level: self.go_back_to_level(tgt_level) elif isinstance(self.model, SearchResultsModel) and self.current_level == 0: # In case of remote search, when only the search results are on the stack, # we must keep the txt_filter and category_filter (which contains the search term) # before resetting the view self.reset_view(text_filter=self.model.text_filter, category_filter=self.model.category_filter) else: # Reset the view if the user clicks on the last part of the breadcrumb self.reset_view() def _set_filter_controls_from_model(self): # This should typically be called under freeze_controls context manager content_category = ContentCategories.get(self.model.category_filter) filter_display_name = content_category.long_name if content_category else self.model.category_filter self.category_selector.setCurrentIndex( self.categories.index(filter_display_name) if filter_display_name in self.categories else 0) self.channel_torrents_filter_input.setText(self.model.text_filter or '') def go_back_to_level(self, level): switched_level = False level = int(level) disconnected_current_model = False while 0 <= level < self.current_level: switched_level = True if not disconnected_current_model: disconnected_current_model = True self.disconnect_current_model() self.channels_stack.pop().deleteLater() if switched_level: self.channel_description_container.initialized = False # We block signals to prevent triggering redundant model reloading with self.freeze_controls(): self._set_filter_controls_from_model() # Set filter category selector to correct index corresponding to loaded model self.controller.set_model(self.model) self.connect_current_model() self.update_labels() def on_channel_clicked(self, channel_dict): self.initialize_with_channel(channel_dict) def create_new_channel(self, checked): # pylint: disable=W0613 NewChannelDialog(self, self.model.create_new_channel) def initialize_with_channel(self, channel_info): # Hide the edit controls by default, to prevent the user clicking the buttons prematurely self.hide_all_labels() # Turn off sorting by default to speed up SQL queries if channel_info.get("state") == CHANNEL_STATE.PREVIEW.value: self.push_channels_stack( ChannelPreviewModel(channel_info=channel_info)) else: self.push_channels_stack( self.default_channel_model(channel_info=channel_info)) self.controller.set_model(self.model) self.update_navigation_breadcrumbs() self.controller.table_view.deselect_all_rows() self.controller.table_view.resizeEvent(None) self.content_table.setFocus() def update_navigation_breadcrumbs(self): # Assemble the channels navigation breadcrumb by utilising RichText links feature self.channel_name_label.setTextFormat(Qt.RichText) # We build the breadcrumb text backwards, by performing lookahead on each step. # While building the breadcrumb label in RichText we also assemble an undecorated variant of the same text # to estimate if we need to elide the breadcrumb. We cannot use RichText contents directly with # .elidedText method because QT will elide the tags as well. breadcrumb_text = '' breadcrumb_text_undecorated = '' path_parts = [(m, model.channel_info["name"]) for m, model in enumerate(self.channels_stack)] slash_separator = '<font color=#aaa> / </font>' for m, channel_name in reversed(path_parts): breadcrumb_text_undecorated = " / " + channel_name + breadcrumb_text_undecorated breadcrumb_text_elided = self.channel_name_label.fontMetrics( ).elidedText(breadcrumb_text_undecorated, 0, self.channel_name_label.width()) must_elide = breadcrumb_text_undecorated != breadcrumb_text_elided if must_elide: channel_name = "..." breadcrumb_text = ( slash_separator + f'<a style="text-decoration:none;color:#eee;" href="{m}">{channel_name}</a>' + breadcrumb_text) if must_elide: break # Remove the leftmost slash: if len(breadcrumb_text) >= len(slash_separator): breadcrumb_text = breadcrumb_text[len(slash_separator):] self.channel_name_label.setText(breadcrumb_text) self.channel_name_label.setTextInteractionFlags( Qt.TextBrowserInteraction) self.channel_back_button.setHidden(self.current_level == 0) # Disabling focus on the label is necessary to remove the ugly dotted rectangle around the most recently # clicked part of the path. # ACHTUNG! Setting focus policy in the .ui file does not work for some reason! # Also, something changes the focus policy during the runtime, so we have to re-set it every time here. self.channel_name_label.setFocusPolicy(Qt.NoFocus) def update_labels(self): folder = self.model.channel_info.get("type", None) == COLLECTION_NODE personal = self.model.channel_info.get( "state", None) == CHANNEL_STATE.PERSONAL.value root = self.current_level == 0 legacy = self.model.channel_info.get( "state", None) == CHANNEL_STATE.LEGACY.value discovered = isinstance(self.model, DiscoveredChannelsModel) personal_model = isinstance(self.model, PersonalChannelsModel) is_a_channel = self.model.channel_info.get("type", None) == CHANNEL_TORRENT description_flag = self.model.channel_info.get("description_flag") thumbnail_flag = self.model.channel_info.get("thumbnail_flag") dirty = self.model.channel_info.get("dirty") self.update_navigation_breadcrumbs() info = self.model.channel_info container = self.channel_description_container if is_a_channel and (description_flag or thumbnail_flag or personal_model): container.initialize_with_channel(info["public_key"], info["id"], edit=personal and personal_model) else: container.initialized = False container.setHidden(True) self.category_selector.setHidden(root and (discovered or personal_model)) # initialize the channel page self.edit_channel_contents_top_bar.setHidden(not personal) self.new_channel_button.setText( tr("NEW CHANNEL" ) if not is_a_channel and not folder else tr("NEW FOLDER")) self.channel_options_button.setHidden(not personal_model or not personal or (root and not is_a_channel)) self.new_channel_button.setHidden(not personal_model or not personal) self.channel_state_label.setText( self.model.channel_info.get("state", "This text should not ever be shown")) self.subscription_widget.setHidden(not is_a_channel or personal or folder or legacy) if not self.subscription_widget.isHidden(): self.subscription_widget.update_subscribe_button( self.model.channel_info) self.channel_state_label.setHidden((root and not is_a_channel) or personal) self.commit_control_bar.setHidden(self.autocommit_enabled or not dirty or not personal) if "total" in self.model.channel_info: self.channel_num_torrents_label.setHidden(False) if "torrents" in self.model.channel_info: self.channel_num_torrents_label.setText( tr("%(total)i/%(torrents)i items") % self.model.channel_info) else: self.channel_num_torrents_label.setText( tr("%(total)i items") % self.model.channel_info) else: self.channel_num_torrents_label.setHidden(True) # ============================== # Channel menu related methods. # ============================== def create_channel_options_menu(self): browse_files_action = QAction(tr("Add .torrent file"), self) browse_dir_action = QAction(tr("Add torrent(s) directory"), self) add_url_action = QAction(tr("Add URL/magnet links"), self) connect(browse_files_action.triggered, self.on_add_torrent_browse_file) connect(browse_dir_action.triggered, self.on_add_torrents_browse_dir) connect(add_url_action.triggered, self.on_add_torrent_from_url) channel_options_menu = TriblerActionMenu(self) channel_options_menu.addAction(browse_files_action) channel_options_menu.addAction(browse_dir_action) channel_options_menu.addAction(add_url_action) return channel_options_menu # Torrent addition-related methods def on_add_torrents_browse_dir(self, checked): # pylint: disable=W0613 chosen_dir = QFileDialog.getExistingDirectory( self, tr("Please select the directory containing the .torrent files"), QDir.homePath(), QFileDialog.ShowDirsOnly, ) if not chosen_dir: return self.chosen_dir = chosen_dir self.dialog = ConfirmationDialog( self, tr("Add torrents from directory"), tr("Add all torrent files from the following directory to your Tribler channel: \n\n %s" ) % chosen_dir, [('ADD', BUTTON_TYPE_NORMAL), ('CANCEL', BUTTON_TYPE_CONFIRM)], checkbox_text=tr("Include subdirectories (recursive mode)"), ) connect(self.dialog.button_clicked, self.on_confirm_add_directory_dialog) self.dialog.show() def on_confirm_add_directory_dialog(self, action): if action == 0: self.add_dir_to_channel(self.chosen_dir, recursive=self.dialog.checkbox.isChecked()) if self.dialog: self.dialog.close_dialog() self.dialog = None self.chosen_dir = None def on_add_torrent_browse_file(self, checked): # pylint: disable=W0613 filenames = QFileDialog.getOpenFileNames( self, tr("Please select the .torrent file"), filter=(tr("Torrent files %s") % '(*.torrent)')) if not filenames[0]: return for filename in filenames[0]: self.add_torrent_to_channel(filename) def on_add_torrent_from_url(self, checked): # pylint: disable=W0613 self.dialog = ConfirmationDialog( self, tr("Add torrent from URL/magnet link"), tr("Please enter the URL/magnet link in the field below:"), [(tr("ADD"), BUTTON_TYPE_NORMAL), (tr("CANCEL"), BUTTON_TYPE_CONFIRM)], show_input=True, ) self.dialog.dialog_widget.dialog_input.setPlaceholderText( tr("URL/magnet link")) connect(self.dialog.button_clicked, self.on_torrent_from_url_dialog_done) self.dialog.show() def on_torrent_from_url_dialog_done(self, action): if action == 0: self.add_torrent_url_to_channel( self.dialog.dialog_widget.dialog_input.text()) self.dialog.close_dialog() self.dialog = None def _on_torrent_to_channel_added(self, result): if not result: return if result.get('added'): # ACHTUNG: this is a dumb hack to adapt torrents PUT endpoint output to the info_changed signal. # If thousands of torrents were added, we don't want to post them all in a single # REST response. Instead, we always provide the total number of new torrents. # If we add a single torrent though, the endpoint will return it as a dict. # However, on_model_info_changed always expects a list of changed entries. # So, we make up the list. results_list = result['added'] if isinstance(results_list, dict): results_list = [results_list] elif isinstance(results_list, int): results_list = [{'status': NEW}] self.model.info_changed.emit(results_list) self.model.reset() def _add_torrent_request(self, data): TriblerNetworkRequest( f'collections/mychannel/{self.model.channel_info["id"]}/torrents', self._on_torrent_to_channel_added, method='PUT', data=data, ) def add_torrent_to_channel(self, filename): with open(filename, "rb") as torrent_file: torrent_content = b64encode(torrent_file.read()).decode('utf-8') self._add_torrent_request({"torrent": torrent_content}) def add_dir_to_channel(self, dirname, recursive=False): self._add_torrent_request({ "torrents_dir": dirname, "recursive": int(recursive) }) def add_torrent_url_to_channel(self, url): self._add_torrent_request({"uri": url})
def save_settings(self): # Create a dictionary with all available settings settings_data = { 'general': {}, 'Tribler': {}, 'download_defaults': {}, 'libtorrent': {}, 'watch_folder': {}, 'tunnel_community': {}, 'market_community': {}, 'trustchain': {}, 'resource_monitor': {}, 'ipv8': {}, 'chant': {}, } settings_data['download_defaults']['saveas'] = self.window().download_location_input.text() settings_data['general']['log_dir'] = self.window().log_location_input.text() settings_data['watch_folder']['enabled'] = self.window().watchfolder_enabled_checkbox.isChecked() if settings_data['watch_folder']['enabled']: settings_data['watch_folder']['directory'] = self.window().watchfolder_location_input.text() settings_data['market_community']['enabled'] = self.window().enable_market_checkbox.isChecked() settings_data['libtorrent']['proxy_type'] = self.window().lt_proxy_type_combobox.currentIndex() if ( self.window().lt_proxy_server_input.text() and len(self.window().lt_proxy_server_input.text()) > 0 and len(self.window().lt_proxy_port_input.text()) > 0 ): try: settings_data['libtorrent']['proxy_server'] = "%s:%s" % ( self.window().lt_proxy_server_input.text(), int(self.window().lt_proxy_port_input.text()), ) except ValueError: ConfirmationDialog.show_error( self.window(), "Invalid proxy port number", "You've entered an invalid format for the proxy port number. " "Please enter a whole number.", ) return else: settings_data['libtorrent']['proxy_server'] = ":" if self.window().lt_proxy_username_input.text() and self.window().lt_proxy_password_input.text(): settings_data['libtorrent']['proxy_auth'] = "%s:%s" % ( self.window().lt_proxy_username_input.text(), self.window().lt_proxy_password_input.text(), ) else: settings_data['libtorrent']['proxy_auth'] = ":" settings_data['libtorrent']['utp'] = self.window().lt_utp_checkbox.isChecked() try: max_conn_download = int(self.window().max_connections_download_input.text()) except ValueError: ConfirmationDialog.show_error( self.window(), "Invalid number of connections", "You've entered an invalid format for the maximum number of connections. " "Please enter a whole number.", ) return if max_conn_download == 0: max_conn_download = -1 settings_data['libtorrent']['max_connections_download'] = max_conn_download try: if self.window().upload_rate_limit_input.text(): user_upload_rate_limit = int(self.window().upload_rate_limit_input.text()) * 1024 if user_upload_rate_limit < sys.maxsize: settings_data['libtorrent']['max_upload_rate'] = user_upload_rate_limit else: raise ValueError if self.window().download_rate_limit_input.text(): user_download_rate_limit = int(self.window().download_rate_limit_input.text()) * 1024 if user_download_rate_limit < sys.maxsize: settings_data['libtorrent']['max_download_rate'] = user_download_rate_limit else: raise ValueError except ValueError: ConfirmationDialog.show_error( self.window(), "Invalid value for bandwidth limit", "You've entered an invalid value for the maximum upload/download rate. " "Please enter a whole number (max: %d)" % (sys.maxsize / 1000), ) return try: if self.window().api_port_input.text(): api_port = int(self.window().api_port_input.text()) if api_port <= 0 or api_port >= 65536: raise ValueError() self.window().gui_settings.setValue("api_port", api_port) except ValueError: ConfirmationDialog.show_error( self.window(), "Invalid value for api port", "Please enter a valid port for the api (between 0 and 65536)", ) return seeding_modes = ['forever', 'time', 'never', 'ratio'] selected_mode = 'forever' for seeding_mode in seeding_modes: if getattr(self.window(), "seeding_" + seeding_mode + "_radio").isChecked(): selected_mode = seeding_mode break settings_data['download_defaults']['seeding_mode'] = selected_mode settings_data['download_defaults']['seeding_ratio'] = float(self.window().seeding_ratio_combobox.currentText()) try: settings_data['download_defaults']['seeding_time'] = string_to_seconds( self.window().seeding_time_input.text() ) except ValueError: ConfirmationDialog.show_error( self.window(), "Invalid seeding time", "You've entered an invalid format for the seeding time (expected HH:MM)", ) return settings_data['tunnel_community']['exitnode_enabled'] = self.window().allow_exit_node_checkbox.isChecked() settings_data['download_defaults']['number_hops'] = self.window().number_hops_slider.value() settings_data['download_defaults'][ 'anonymity_enabled' ] = self.window().download_settings_anon_checkbox.isChecked() settings_data['download_defaults'][ 'safeseeding_enabled' ] = self.window().download_settings_anon_seeding_checkbox.isChecked() settings_data['download_defaults'][ 'add_download_to_channel' ] = self.window().download_settings_add_to_channel_checkbox.isChecked() settings_data['resource_monitor']['enabled'] = self.window().checkbox_enable_resource_log.isChecked() settings_data['resource_monitor']['cpu_priority'] = int(self.window().slider_cpu_level.value()) # network statistics settings_data['ipv8']['statistics'] = self.window().checkbox_enable_network_statistics.isChecked() self.window().settings_save_button.setEnabled(False) TriblerNetworkRequest("settings", self.on_settings_saved, method='POST', raw_data=json.dumps(settings_data))
class TriblerWindow(QMainWindow): resize_event = pyqtSignal() escape_pressed = pyqtSignal() tribler_crashed = pyqtSignal(str) received_search_completions = pyqtSignal(object) def __init__(self, core_args=None, core_env=None, api_port=None, api_key=None): QMainWindow.__init__(self) self._logger = logging.getLogger(self.__class__.__name__) QCoreApplication.setOrganizationDomain("nl") QCoreApplication.setOrganizationName("TUDelft") QCoreApplication.setApplicationName("Tribler") self.setWindowIcon(QIcon(QPixmap(get_image_path('tribler.png')))) self.gui_settings = QSettings('nl.tudelft.tribler') api_port = api_port or int(get_gui_setting(self.gui_settings, "api_port", DEFAULT_API_PORT)) api_key = api_key or get_gui_setting(self.gui_settings, "api_key", hexlify(os.urandom(16)).encode('utf-8')) self.gui_settings.setValue("api_key", api_key) api_port = get_first_free_port(start=api_port, limit=100) request_manager.port, request_manager.key = api_port, api_key self.tribler_started = False self.tribler_settings = None # TODO: move version_id to tribler_common and get core version in the core crash message self.tribler_version = version_id self.debug_window = None self.error_handler = ErrorHandler(self) self.core_manager = CoreManager(api_port, api_key, self.error_handler) self.pending_requests = {} self.pending_uri_requests = [] self.download_uri = None self.dialog = None self.create_dialog = None self.chosen_dir = None self.new_version_dialog = None self.start_download_dialog_active = False self.selected_torrent_files = [] self.has_search_results = False self.last_search_query = None self.last_search_time = None self.start_time = time.time() self.token_refresh_timer = None self.shutdown_timer = None self.add_torrent_url_dialog_active = False sys.excepthook = self.error_handler.gui_error uic.loadUi(get_ui_file_path('mainwindow.ui'), self) TriblerRequestManager.window = self self.tribler_status_bar.hide() self.token_balance_widget.mouseReleaseEvent = self.on_token_balance_click def on_state_update(new_state): self.loading_text_label.setText(new_state) connect(self.core_manager.core_state_update, on_state_update) self.magnet_handler = MagnetHandler(self.window) QDesktopServices.setUrlHandler("magnet", self.magnet_handler, "on_open_magnet_link") self.debug_pane_shortcut = QShortcut(QKeySequence("Ctrl+d"), self) connect(self.debug_pane_shortcut.activated, self.clicked_menu_button_debug) self.import_torrent_shortcut = QShortcut(QKeySequence("Ctrl+o"), self) connect(self.import_torrent_shortcut.activated, self.on_add_torrent_browse_file) self.add_torrent_url_shortcut = QShortcut(QKeySequence("Ctrl+i"), self) connect(self.add_torrent_url_shortcut.activated, self.on_add_torrent_from_url) connect(self.top_search_bar.clicked, self.clicked_search_bar) # Remove the focus rect on OS X for widget in self.findChildren(QLineEdit) + self.findChildren(QListWidget) + self.findChildren(QTreeWidget): widget.setAttribute(Qt.WA_MacShowFocusRect, 0) self.menu_buttons = [ self.left_menu_button_downloads, self.left_menu_button_discovered, self.left_menu_button_trust_graph, self.left_menu_button_popular, ] hide_xxx = get_gui_setting(self.gui_settings, "family_filter", True, is_bool=True) self.search_results_page.initialize_content_page(hide_xxx=hide_xxx) self.search_results_page.channel_torrents_filter_input.setHidden(True) self.settings_page.initialize_settings_page() self.downloads_page.initialize_downloads_page() self.loading_page.initialize_loading_page() self.discovering_page.initialize_discovering_page() self.discovered_page.initialize_content_page(hide_xxx=hide_xxx) self.popular_page.initialize_content_page(hide_xxx=hide_xxx) self.trust_page.initialize_trust_page() self.trust_graph_page.initialize_trust_graph() self.stackedWidget.setCurrentIndex(PAGE_LOADING) # Create the system tray icon if QSystemTrayIcon.isSystemTrayAvailable(): self.tray_icon = QSystemTrayIcon() use_monochrome_icon = get_gui_setting(self.gui_settings, "use_monochrome_icon", False, is_bool=True) self.update_tray_icon(use_monochrome_icon) # Create the tray icon menu menu = self.create_add_torrent_menu() show_downloads_action = QAction('Show downloads', self) connect(show_downloads_action.triggered, self.clicked_menu_button_downloads) token_balance_action = QAction('Show token balance', self) connect(token_balance_action.triggered, lambda _: self.on_token_balance_click(None)) quit_action = QAction('Quit Tribler', self) connect(quit_action.triggered, self.close_tribler) menu.addSeparator() menu.addAction(show_downloads_action) menu.addAction(token_balance_action) menu.addSeparator() menu.addAction(quit_action) self.tray_icon.setContextMenu(menu) else: self.tray_icon = None self.left_menu_button_debug.setHidden(True) self.top_menu_button.setHidden(True) self.left_menu.setHidden(True) self.token_balance_widget.setHidden(True) self.settings_button.setHidden(True) self.add_torrent_button.setHidden(True) self.top_search_bar.setHidden(True) # Set various icons self.top_menu_button.setIcon(QIcon(get_image_path('menu.png'))) self.search_completion_model = QStringListModel() completer = QCompleter() completer.setModel(self.search_completion_model) completer.setCompletionMode(QCompleter.UnfilteredPopupCompletion) self.item_delegate = QStyledItemDelegate() completer.popup().setItemDelegate(self.item_delegate) completer.popup().setStyleSheet( """ QListView { background-color: #404040; } QListView::item { color: #D0D0D0; padding-top: 5px; padding-bottom: 5px; } QListView::item:hover { background-color: #707070; } """ ) self.top_search_bar.setCompleter(completer) # Toggle debug if developer mode is enabled self.window().left_menu_button_debug.setHidden( not get_gui_setting(self.gui_settings, "debug", False, is_bool=True) ) # Start Tribler self.core_manager.start(core_args=core_args, core_env=core_env) connect(self.core_manager.events_manager.torrent_finished, self.on_torrent_finished) connect(self.core_manager.events_manager.new_version_available, self.on_new_version_available) connect(self.core_manager.events_manager.tribler_started, self.on_tribler_started) connect(self.core_manager.events_manager.low_storage_signal, self.on_low_storage) connect(self.core_manager.events_manager.tribler_shutdown_signal, self.on_tribler_shutdown_state_update) connect(self.core_manager.events_manager.config_error_signal, self.on_config_error_signal) # Install signal handler for ctrl+c events def sigint_handler(*_): self.close_tribler() signal.signal(signal.SIGINT, sigint_handler) # Resize the window according to the settings center = QApplication.desktop().availableGeometry(self).center() pos = self.gui_settings.value("pos", QPoint(center.x() - self.width() * 0.5, center.y() - self.height() * 0.5)) size = self.gui_settings.value("size", self.size()) self.move(pos) self.resize(size) self.show() self.add_to_channel_dialog = AddToChannelDialog(self.window()) self.add_torrent_menu = self.create_add_torrent_menu() self.add_torrent_button.setMenu(self.add_torrent_menu) self.channels_menu_list = self.findChild(ChannelsMenuListWidget, "channels_menu_list") connect(self.channels_menu_list.itemClicked, self.open_channel_contents_page) # The channels content page is only used to show subscribed channels, so we always show xxx # contents in it. connect( self.core_manager.events_manager.node_info_updated, lambda data: self.channels_menu_list.reload_if_necessary([data]), ) connect(self.left_menu_button_new_channel.clicked, self.create_new_channel) def create_new_channel(self, checked): # TODO: DRY this with tablecontentmodel, possibly using QActions def create_channel_callback(channel_name): TriblerNetworkRequest( "channels/mychannel/0/channels", self.channels_menu_list.load_channels, method='POST', raw_data=json.dumps({"name": channel_name}) if channel_name else None, ) NewChannelDialog(self, create_channel_callback) def open_channel_contents_page(self, channel_list_item): if not channel_list_item.flags() & Qt.ItemIsEnabled: return self.channel_contents_page.initialize_root_model_from_channel_info(channel_list_item.channel_info) self.stackedWidget.setCurrentIndex(PAGE_CHANNEL_CONTENTS) self.deselect_all_menu_buttons() def update_tray_icon(self, use_monochrome_icon): if not QSystemTrayIcon.isSystemTrayAvailable() or not self.tray_icon: return if use_monochrome_icon: self.tray_icon.setIcon(QIcon(QPixmap(get_image_path('monochrome_tribler.png')))) else: self.tray_icon.setIcon(QIcon(QPixmap(get_image_path('tribler.png')))) self.tray_icon.show() def delete_tray_icon(self): if self.tray_icon: try: self.tray_icon.deleteLater() except RuntimeError: # The tray icon might have already been removed when unloading Qt. # This is due to the C code actually being asynchronous. logging.debug("Tray icon already removed, no further deletion necessary.") self.tray_icon = None def on_low_storage(self, _): """ Dealing with low storage space available. First stop the downloads and the core manager and ask user to user to make free space. :return: """ def close_tribler_gui(): self.close_tribler() # Since the core has already stopped at this point, it will not terminate the GUI. # So, we quit the GUI separately here. if not QApplication.closingDown(): QApplication.quit() self.downloads_page.stop_loading_downloads() self.core_manager.stop(False) close_dialog = ConfirmationDialog( self.window(), "<b>CRITICAL ERROR</b>", "You are running low on disk space (<100MB). Please make sure to have " "sufficient free space available and restart Tribler again.", [("Close Tribler", BUTTON_TYPE_NORMAL)], ) connect(close_dialog.button_clicked, lambda _: close_tribler_gui()) close_dialog.show() def on_torrent_finished(self, torrent_info): if "hidden" not in torrent_info or not torrent_info["hidden"]: self.tray_show_message("Download finished", f"Download of {torrent_info['name']} has finished.") def show_loading_screen(self): self.top_menu_button.setHidden(True) self.left_menu.setHidden(True) self.token_balance_widget.setHidden(True) self.settings_button.setHidden(True) self.add_torrent_button.setHidden(True) self.top_search_bar.setHidden(True) self.stackedWidget.setCurrentIndex(PAGE_LOADING) def tray_set_tooltip(self, message): """ Set a tooltip message for the tray icon, if possible. :param message: the message to display on hover """ if self.tray_icon: try: self.tray_icon.setToolTip(message) except RuntimeError as e: logging.error("Failed to set tray tooltip: %s", str(e)) def tray_show_message(self, title, message): """ Show a message at the tray icon, if possible. :param title: the title of the message :param message: the message to display """ if self.tray_icon: try: self.tray_icon.showMessage(title, message) except RuntimeError as e: logging.error("Failed to set tray message: %s", str(e)) def on_tribler_started(self, version): if self.tribler_started: logging.warning("Received duplicate Tribler Core started event") return self.tribler_started = True self.tribler_version = version self.top_menu_button.setHidden(False) self.left_menu.setHidden(False) self.token_balance_widget.setHidden(False) self.settings_button.setHidden(False) self.add_torrent_button.setHidden(False) self.top_search_bar.setHidden(False) self.fetch_settings() self.downloads_page.start_loading_downloads() self.setAcceptDrops(True) self.setWindowTitle(f"Tribler {self.tribler_version}") autocommit_enabled = ( get_gui_setting(self.gui_settings, "autocommit_enabled", True, is_bool=True) if self.gui_settings else True ) self.channel_contents_page.initialize_content_page(autocommit_enabled=autocommit_enabled, hide_xxx=False) hide_xxx = get_gui_setting(self.gui_settings, "family_filter", True, is_bool=True) self.discovered_page.initialize_root_model( DiscoveredChannelsModel( channel_info={"name": "Discovered channels"}, endpoint_url="channels", hide_xxx=hide_xxx ) ) connect(self.core_manager.events_manager.discovered_channel, self.discovered_page.model.on_new_entry_received) self.popular_page.initialize_root_model( PopularTorrentsModel(channel_info={"name": "Popular torrents"}, hide_xxx=hide_xxx) ) self.add_to_channel_dialog.load_channel(0) if not self.gui_settings.value("first_discover", False) and not self.core_manager.use_existing_core: connect(self.core_manager.events_manager.discovered_channel, self.stop_discovering) self.window().gui_settings.setValue("first_discover", True) self.discovering_page.is_discovering = True self.stackedWidget.setCurrentIndex(PAGE_DISCOVERING) else: self.clicked_menu_button_discovered() self.left_menu_button_discovered.setChecked(True) self.channels_menu_list.load_channels() def stop_discovering(self, response): if not self.discovering_page.is_discovering: return disconnect(self.core_manager.events_manager.discovered_channel, self.stop_discovering) self.discovering_page.is_discovering = False if self.stackedWidget.currentIndex() == PAGE_DISCOVERING: self.clicked_menu_button_discovered() self.left_menu_button_discovered.setChecked(True) def on_events_started(self, json_dict): self.setWindowTitle(f"Tribler {json_dict['version']}") def show_status_bar(self, message): self.tribler_status_bar_label.setText(message) self.tribler_status_bar.show() def hide_status_bar(self): self.tribler_status_bar.hide() def process_uri_request(self): """ Process a URI request if we have one in the queue. """ if len(self.pending_uri_requests) == 0: return uri = self.pending_uri_requests.pop() if uri.startswith('file') or uri.startswith('magnet'): self.start_download_from_uri(uri) def update_recent_download_locations(self, destination): # Save the download location to the GUI settings current_settings = get_gui_setting(self.gui_settings, "recent_download_locations", "") recent_locations = current_settings.split(",") if len(current_settings) > 0 else [] if isinstance(destination, str): destination = destination.encode('utf-8') encoded_destination = hexlify(destination) if encoded_destination in recent_locations: recent_locations.remove(encoded_destination) recent_locations.insert(0, encoded_destination) if len(recent_locations) > 5: recent_locations = recent_locations[:5] self.gui_settings.setValue("recent_download_locations", ','.join(recent_locations)) def perform_start_download_request( self, uri, anon_download, safe_seeding, destination, selected_files, total_files=0, add_to_channel=False, callback=None, ): # Check if destination directory is writable is_writable, error = is_dir_writable(destination) if not is_writable: gui_error_message = ( "Insufficient write permissions to <i>%s</i> directory. Please add proper " "write permissions on the directory and add the torrent again. %s" % (destination, error) ) ConfirmationDialog.show_message(self.window(), f"Download error <i>{uri}</i>", gui_error_message, "OK") return selected_files_list = [] if len(selected_files) != total_files: # Not all files included selected_files_list = [filename for filename in selected_files] anon_hops = int(self.tribler_settings['download_defaults']['number_hops']) if anon_download else 0 safe_seeding = 1 if safe_seeding else 0 post_data = { "uri": uri, "anon_hops": anon_hops, "safe_seeding": safe_seeding, "destination": destination, "selected_files": selected_files_list, } TriblerNetworkRequest( "downloads", callback if callback else self.on_download_added, method='PUT', data=post_data ) self.update_recent_download_locations(destination) if add_to_channel: def on_add_button_pressed(channel_id): post_data = {} if uri.startswith("file:"): with open(uri[5:], "rb") as torrent_file: post_data['torrent'] = b64encode(torrent_file.read()).decode('utf8') elif uri.startswith("magnet:"): post_data['uri'] = uri if post_data: TriblerNetworkRequest( f"channels/mychannel/{channel_id}/torrents", lambda _: self.tray_show_message("Channel update", "Torrent(s) added to your channel"), method='PUT', data=post_data, ) self.window().add_to_channel_dialog.show_dialog(on_add_button_pressed, confirm_button_text="Add torrent") def on_new_version_available(self, version): if version == str(self.gui_settings.value('last_reported_version')): return # To prevent multiple dialogs on top of each other, # close any existing dialog first. if self.new_version_dialog: self.new_version_dialog.close_dialog() self.new_version_dialog = None self.new_version_dialog = ConfirmationDialog( self, "New version available", "Version %s of Tribler is available.Do you want to visit the " "website to download the newest version?" % version, [('IGNORE', BUTTON_TYPE_NORMAL), ('LATER', BUTTON_TYPE_NORMAL), ('OK', BUTTON_TYPE_NORMAL)], ) connect(self.new_version_dialog.button_clicked, lambda action: self.on_new_version_dialog_done(version, action)) self.new_version_dialog.show() def on_new_version_dialog_done(self, version, action): if action == 0: # ignore self.gui_settings.setValue("last_reported_version", version) elif action == 2: # ok import webbrowser webbrowser.open("https://tribler.org") if self.new_version_dialog: self.new_version_dialog.close_dialog() self.new_version_dialog = None def on_search_text_change(self, text): # We do not want to bother the database on petty 1-character queries if len(text) < 2: return TriblerNetworkRequest( "search/completions", self.on_received_search_completions, url_params={'q': sanitize_for_fts(text)} ) def on_received_search_completions(self, completions): if completions is None: return self.received_search_completions.emit(completions) self.search_completion_model.setStringList(completions["completions"]) def fetch_settings(self): TriblerNetworkRequest("settings", self.received_settings, capture_core_errors=False) def received_settings(self, settings): if not settings: return # If we cannot receive the settings, stop Tribler with an option to send the crash report. if 'error' in settings: raise RuntimeError(TriblerRequestManager.get_message_from_error(settings)) # If there is any pending dialog (likely download dialog or error dialog of setting not available), # close the dialog if self.dialog: self.dialog.close_dialog() self.dialog = None self.tribler_settings = settings['settings'] self.downloads_all_button.click() # process pending file requests (i.e. someone clicked a torrent file when Tribler was closed) # We do this after receiving the settings so we have the default download location. self.process_uri_request() # Set token balance refresh timer and load the token balance self.token_refresh_timer = QTimer() connect(self.token_refresh_timer.timeout, self.load_token_balance) self.token_refresh_timer.start(60000) self.load_token_balance() def on_top_search_button_click(self): current_ts = time.time() query = self.top_search_bar.text() if ( self.last_search_query and self.last_search_time and self.last_search_query == self.top_search_bar.text() and current_ts - self.last_search_time < 1 ): logging.info("Same search query already sent within 500ms so dropping this one") return if not query: return self.has_search_results = True self.search_results_page.initialize_root_model( SearchResultsModel( channel_info={"name": f"Search results for {query}" if len(query) < 50 else f"{query[:50]}..."}, endpoint_url="search", hide_xxx=get_gui_setting(self.gui_settings, "family_filter", True, is_bool=True), text_filter=to_fts_query(query), ) ) self.clicked_search_bar() # Trigger remote search self.search_results_page.preview_clicked() self.last_search_query = query self.last_search_time = current_ts def on_settings_button_click(self): self.deselect_all_menu_buttons() self.stackedWidget.setCurrentIndex(PAGE_SETTINGS) self.settings_page.load_settings() def on_token_balance_click(self, _): self.raise_window() self.deselect_all_menu_buttons() self.stackedWidget.setCurrentIndex(PAGE_TRUST) self.load_token_balance() self.trust_page.load_history() def load_token_balance(self): TriblerNetworkRequest("bandwidth/statistics", self.received_bandwidth_statistics, capture_core_errors=False) def received_bandwidth_statistics(self, statistics): if not statistics or "statistics" not in statistics: return self.trust_page.received_bandwidth_statistics(statistics) statistics = statistics["statistics"] balance = statistics["total_given"] - statistics["total_taken"] self.set_token_balance(balance) # If trust page is currently visible, then load the graph as well if self.stackedWidget.currentIndex() == PAGE_TRUST: self.trust_page.load_history() def set_token_balance(self, balance): if abs(balance) > 1024 ** 4: # Balance is over a TB balance /= 1024.0 ** 4 self.token_balance_label.setText(f"{balance:.1f} TB") elif abs(balance) > 1024 ** 3: # Balance is over a GB balance /= 1024.0 ** 3 self.token_balance_label.setText(f"{balance:.1f} GB") else: balance /= 1024.0 ** 2 self.token_balance_label.setText("%d MB" % balance) def raise_window(self): self.setWindowState(self.windowState() & ~Qt.WindowMinimized | Qt.WindowActive) self.raise_() self.activateWindow() def create_add_torrent_menu(self): """ Create a menu to add new torrents. Shows when users click on the tray icon or the big plus button. """ menu = TriblerActionMenu(self) browse_files_action = QAction('Import torrent from file', self) browse_directory_action = QAction('Import torrent(s) from directory', self) add_url_action = QAction('Import torrent from magnet/URL', self) create_torrent_action = QAction('Create torrent from file(s)', self) connect(browse_files_action.triggered, self.on_add_torrent_browse_file) connect(browse_directory_action.triggered, self.on_add_torrent_browse_dir) connect(add_url_action.triggered, self.on_add_torrent_from_url) connect(create_torrent_action.triggered, self.on_create_torrent) menu.addAction(browse_files_action) menu.addAction(browse_directory_action) menu.addAction(add_url_action) menu.addSeparator() menu.addAction(create_torrent_action) return menu def on_create_torrent(self, checked): if self.create_dialog: self.create_dialog.close_dialog() self.create_dialog = CreateTorrentDialog(self) connect(self.create_dialog.create_torrent_notification, self.on_create_torrent_updates) self.create_dialog.show() def on_create_torrent_updates(self, update_dict): self.tray_show_message("Torrent updates", update_dict['msg']) def on_add_torrent_browse_file(self, index): filenames = QFileDialog.getOpenFileNames( self, "Please select the .torrent file", QDir.homePath(), "Torrent files (*.torrent)" ) if len(filenames[0]) > 0: for filename in filenames[0]: self.pending_uri_requests.append(f"file:{filename}") self.process_uri_request() def start_download_from_uri(self, uri): uri = uri.decode('utf-8') if isinstance(uri, bytes) else uri self.download_uri = uri if get_gui_setting(self.gui_settings, "ask_download_settings", True, is_bool=True): # FIXME: instead of using this workaround, make sure the settings are _available_ by this moment # If tribler settings is not available, fetch the settings and inform the user to try again. if not self.tribler_settings: self.fetch_settings() self.dialog = ConfirmationDialog.show_error( self, "Download Error", "Tribler settings is not available\ yet. Fetching it now. Please try again later.", ) # By re-adding the download uri to the pending list, the request is re-processed # when the settings is received self.pending_uri_requests.append(uri) return # Clear any previous dialog if exists if self.dialog: self.dialog.close_dialog() self.dialog = None self.dialog = StartDownloadDialog(self, self.download_uri) connect(self.dialog.button_clicked, self.on_start_download_action) self.dialog.show() self.start_download_dialog_active = True else: # FIXME: instead of using this workaround, make sure the settings are _available_ by this moment # In the unlikely scenario that tribler settings are not available yet, try to fetch settings again and # add the download uri back to self.pending_uri_requests to process again. if not self.tribler_settings: self.fetch_settings() if self.download_uri not in self.pending_uri_requests: self.pending_uri_requests.append(self.download_uri) return self.window().perform_start_download_request( self.download_uri, self.window().tribler_settings['download_defaults']['anonymity_enabled'], self.window().tribler_settings['download_defaults']['safeseeding_enabled'], self.tribler_settings['download_defaults']['saveas'], [], ) self.process_uri_request() def on_start_download_action(self, action): if action == 1: if self.dialog and self.dialog.dialog_widget: self.window().perform_start_download_request( self.download_uri, self.dialog.dialog_widget.anon_download_checkbox.isChecked(), self.dialog.dialog_widget.safe_seed_checkbox.isChecked(), self.dialog.dialog_widget.destination_input.currentText(), self.dialog.get_selected_files(), self.dialog.dialog_widget.files_list_view.topLevelItemCount(), add_to_channel=self.dialog.dialog_widget.add_to_channel_checkbox.isChecked(), ) else: ConfirmationDialog.show_error(self, "Tribler UI Error", "Something went wrong. Please try again.") logging.exception("Error while trying to download. Either dialog or dialog.dialog_widget is None") if self.dialog: self.dialog.close_dialog() self.dialog = None self.start_download_dialog_active = False if action == 0: # We do this after removing the dialog since process_uri_request is blocking self.process_uri_request() def on_add_torrent_browse_dir(self, checked): chosen_dir = QFileDialog.getExistingDirectory( self, "Please select the directory containing the .torrent files", QDir.homePath(), QFileDialog.ShowDirsOnly ) self.chosen_dir = chosen_dir if len(chosen_dir) != 0: self.selected_torrent_files = [torrent_file for torrent_file in glob.glob(chosen_dir + "/*.torrent")] self.dialog = ConfirmationDialog( self, "Add torrents from directory", "Add %s torrent files from the following directory " "to your Tribler channel:\n\n%s" % (len(self.selected_torrent_files), chosen_dir), [('ADD', BUTTON_TYPE_NORMAL), ('CANCEL', BUTTON_TYPE_CONFIRM)], checkbox_text="Add torrents to My Channel", ) connect(self.dialog.button_clicked, self.on_confirm_add_directory_dialog) self.dialog.show() def on_confirm_add_directory_dialog(self, action): if action == 0: if self.dialog.checkbox.isChecked(): # TODO: add recursive directory scanning def on_add_button_pressed(channel_id): TriblerNetworkRequest( f"collections/mychannel/{channel_id}/torrents", lambda _: self.tray_show_message("Channels update", f"{self.chosen_dir} added to your channel"), method='PUT', data={"torrents_dir": self.chosen_dir}, ) self.window().add_to_channel_dialog.show_dialog( on_add_button_pressed, confirm_button_text="Add torrent(s)" ) for torrent_file in self.selected_torrent_files: self.perform_start_download_request( f"file:{torrent_file}", self.window().tribler_settings['download_defaults']['anonymity_enabled'], self.window().tribler_settings['download_defaults']['safeseeding_enabled'], self.tribler_settings['download_defaults']['saveas'], [], ) if self.dialog: self.dialog.close_dialog() self.dialog = None def on_add_torrent_from_url(self, checked=False): # Make sure that the window is visible (this action might be triggered from the tray icon) self.raise_window() if not self.add_torrent_url_dialog_active: self.dialog = ConfirmationDialog( self, "Add torrent from URL/magnet link", "Please enter the URL/magnet link in the field below:", [('ADD', BUTTON_TYPE_NORMAL), ('CANCEL', BUTTON_TYPE_CONFIRM)], show_input=True, ) self.dialog.dialog_widget.dialog_input.setPlaceholderText('URL/magnet link') self.dialog.dialog_widget.dialog_input.setFocus() connect(self.dialog.button_clicked, self.on_torrent_from_url_dialog_done) self.dialog.show() self.add_torrent_url_dialog_active = True def on_torrent_from_url_dialog_done(self, action): self.add_torrent_url_dialog_active = False if self.dialog and self.dialog.dialog_widget: uri = self.dialog.dialog_widget.dialog_input.text().strip() # If the URI is a 40-bytes hex-encoded infohash, convert it to a valid magnet link if len(uri) == 40: valid_ih_hex = True try: int(uri, 16) except ValueError: valid_ih_hex = False if valid_ih_hex: uri = "magnet:?xt=urn:btih:" + uri # Remove first dialog self.dialog.close_dialog() self.dialog = None if action == 0: self.start_download_from_uri(uri) def on_download_added(self, result): if not result: return if len(self.pending_uri_requests) == 0: # Otherwise, we first process the remaining requests. self.window().left_menu_button_downloads.click() else: self.process_uri_request() def on_top_menu_button_click(self): if self.left_menu.isHidden(): self.left_menu.show() else: self.left_menu.hide() def deselect_all_menu_buttons(self, except_select=None): for button in self.menu_buttons: if button == except_select: button.setEnabled(False) continue button.setEnabled(True) button.setChecked(False) def clicked_search_bar(self, checked=False): query = self.top_search_bar.text() if query and self.has_search_results: self.deselect_all_menu_buttons() if self.stackedWidget.currentIndex() == PAGE_SEARCH_RESULTS: self.search_results_page.go_back_to_level(0) self.stackedWidget.setCurrentIndex(PAGE_SEARCH_RESULTS) def clicked_menu_button_discovered(self): self.deselect_all_menu_buttons() self.left_menu_button_discovered.setChecked(True) if self.stackedWidget.currentIndex() == PAGE_DISCOVERED: self.discovered_page.go_back_to_level(0) self.discovered_page.reset_view() self.stackedWidget.setCurrentIndex(PAGE_DISCOVERED) self.discovered_page.content_table.setFocus() def clicked_menu_button_popular(self): self.deselect_all_menu_buttons() self.left_menu_button_popular.setChecked(True) # We want to reset the view every time to show updates self.popular_page.go_back_to_level(0) self.popular_page.reset_view() self.stackedWidget.setCurrentIndex(PAGE_POPULAR) self.popular_page.content_table.setFocus() def clicked_menu_button_trust_graph(self): self.deselect_all_menu_buttons(self.left_menu_button_trust_graph) self.stackedWidget.setCurrentIndex(PAGE_TRUST_GRAPH_PAGE) def clicked_menu_button_downloads(self, checked): self.deselect_all_menu_buttons(self.left_menu_button_downloads) self.raise_window() self.left_menu_button_downloads.setChecked(True) self.stackedWidget.setCurrentIndex(PAGE_DOWNLOADS) def clicked_menu_button_debug(self, index=False): if not self.debug_window: self.debug_window = DebugWindow(self.tribler_settings, self.tribler_version) self.debug_window.show() def resizeEvent(self, _): # This thing here is necessary to send the resize event to dialogs, etc. self.resize_event.emit() def close_tribler(self, checked=False): if self.core_manager.shutting_down: return def show_force_shutdown(): self.window().force_shutdown_btn.show() self.delete_tray_icon() self.show_loading_screen() self.hide_status_bar() self.loading_text_label.setText("Shutting down...") if self.debug_window: self.debug_window.setHidden(True) self.shutdown_timer = QTimer() connect(self.shutdown_timer.timeout, show_force_shutdown) self.shutdown_timer.start(SHUTDOWN_WAITING_PERIOD) self.gui_settings.setValue("pos", self.pos()) self.gui_settings.setValue("size", self.size()) if self.core_manager.use_existing_core: # Don't close the core that we are using QApplication.quit() self.core_manager.stop() self.core_manager.shutting_down = True self.downloads_page.stop_loading_downloads() request_manager.clear() # Stop the token balance timer if self.token_refresh_timer: self.token_refresh_timer.stop() def closeEvent(self, close_event): self.close_tribler() close_event.ignore() def dragEnterEvent(self, e): file_urls = [_qurl_to_path(url) for url in e.mimeData().urls()] if e.mimeData().hasUrls() else [] if any(os.path.isfile(filename) for filename in file_urls): e.accept() else: e.ignore() def dropEvent(self, e): file_urls = ( [(_qurl_to_path(url), url.toString()) for url in e.mimeData().urls()] if e.mimeData().hasUrls() else [] ) for filename, fileurl in file_urls: if os.path.isfile(filename): self.start_download_from_uri(fileurl) e.accept() def clicked_force_shutdown(self): process_checker = ProcessChecker() if process_checker.already_running: core_pid = process_checker.get_pid_from_lock_file() os.kill(int(core_pid), 9) # Stop the Qt application QApplication.quit() def clicked_skip_conversion(self): self.dialog = ConfirmationDialog( self, "Abort the conversion of Channels database", "The upgrade procedure is now <b>converting your personal channel</b> and channels " "collected by the previous installation of Tribler.<br>" "Are you sure you want to abort the conversion process?<br><br>" "<p style='color:red'><b> !!! WARNING !!! <br>" "You will lose your personal channel and subscribed channels if you ABORT now! </b> </p> <br>", [('ABORT', BUTTON_TYPE_CONFIRM), ('CONTINUE', BUTTON_TYPE_NORMAL)], ) connect(self.dialog.button_clicked, self.on_skip_conversion_dialog) self.dialog.show() def on_channel_subscribe(self, channel_info): patch_data = [{"public_key": channel_info['public_key'], "id": channel_info['id'], "subscribed": True}] TriblerNetworkRequest( "metadata", lambda data: self.core_manager.events_manager.node_info_updated.emit(data[0]), raw_data=json.dumps(patch_data), method='PATCH', ) def on_channel_unsubscribe(self, channel_info): def _on_unsubscribe_action(action): if action == 0: patch_data = [{"public_key": channel_info['public_key'], "id": channel_info['id'], "subscribed": False}] TriblerNetworkRequest( "metadata", lambda data: self.core_manager.events_manager.node_info_updated.emit(data[0]), raw_data=json.dumps(patch_data), method='PATCH', ) if self.dialog: self.dialog.close_dialog() self.dialog = None self.dialog = ConfirmationDialog( self, "Unsubscribe from channel", "Are you sure you want to <b>unsubscribe</b> from channel<br/>" + '\"' + f"<b>{channel_info['name']}</b>" + '\"' + "<br/>and remove its contents?", [('UNSUBSCRIBE', BUTTON_TYPE_NORMAL), ('CANCEL', BUTTON_TYPE_CONFIRM)], ) connect(self.dialog.button_clicked, _on_unsubscribe_action) self.dialog.show() def on_channel_delete(self, channel_info): def _on_delete_action(action): if action == 0: delete_data = [{"public_key": channel_info['public_key'], "id": channel_info['id']}] TriblerNetworkRequest( "metadata", lambda data: self.core_manager.events_manager.node_info_updated.emit(data[0]), raw_data=json.dumps(delete_data), method='DELETE', ) if self.dialog: self.dialog.close_dialog() self.dialog = None self.dialog = ConfirmationDialog( self, "Delete channel", "Are you sure you want to <b>delete</b> your personal channel<br/>" + '\"' + f"<b>{channel_info['name']}</b>" + '\"' + "<br/>and all its contents?", [('DELETE', BUTTON_TYPE_NORMAL), ('CANCEL', BUTTON_TYPE_CONFIRM)], ) connect(self.dialog.button_clicked, _on_delete_action) self.dialog.show() def on_skip_conversion_dialog(self, action): if action == 0: TriblerNetworkRequest("upgrader", lambda _: None, data={"skip_db_upgrade": True}, method='POST') if self.dialog: self.dialog.close_dialog() self.dialog = None def on_tribler_shutdown_state_update(self, state): self.loading_text_label.setText(state) def on_config_error_signal(self, stacktrace): self._logger.error(f"Config error: {stacktrace}") user_message = 'Tribler recovered from a corrupted config. Please check your settings and update if necessary.' ConfirmationDialog.show_error(self, "Tribler config error", user_message)
def on_config_error_signal(self, stacktrace): self._logger.error(f"Config error: {stacktrace}") user_message = 'Tribler recovered from a corrupted config. Please check your settings and update if necessary.' ConfirmationDialog.show_error(self, "Tribler config error", user_message)
class SettingsPage(QWidget): """ This class is responsible for displaying and adjusting the settings present in Tribler. """ def __init__(self): QWidget.__init__(self) self.settings = None self.saved_dialog = None self.empty_tokens_barcode_dialog = None self.empty_partial_tokens_dialog = None self.confirm_empty_tokens_dialog = None def initialize_settings_page(self): self.window().settings_tab.initialize() self.window().settings_tab.clicked_tab_button.connect(self.clicked_tab_button) self.window().settings_save_button.clicked.connect(self.save_settings) self.window().download_location_chooser_button.clicked.connect(self.on_choose_download_dir_clicked) self.window().watch_folder_chooser_button.clicked.connect(self.on_choose_watch_dir_clicked) self.window().channel_autocommit_checkbox.stateChanged.connect(self.on_channel_autocommit_checkbox_changed) self.window().family_filter_checkbox.stateChanged.connect(self.on_family_filter_checkbox_changed) self.window().developer_mode_enabled_checkbox.stateChanged.connect(self.on_developer_mode_checkbox_changed) self.window().use_monochrome_icon_checkbox.stateChanged.connect(self.on_use_monochrome_icon_checkbox_changed) self.window().download_settings_anon_checkbox.stateChanged.connect(self.on_anon_download_state_changed) self.window().fully_empty_tokens_button.clicked.connect(self.confirm_fully_empty_tokens) self.window().partially_empty_tokens_button.clicked.connect(self.partially_empty_tokens) self.window().log_location_chooser_button.clicked.connect(self.on_choose_log_dir_clicked) checkbox_style = get_checkbox_style() for checkbox in [ self.window().family_filter_checkbox, self.window().channel_autocommit_checkbox, self.window().always_ask_location_checkbox, self.window().developer_mode_enabled_checkbox, self.window().use_monochrome_icon_checkbox, self.window().download_settings_anon_checkbox, self.window().download_settings_anon_seeding_checkbox, self.window().lt_utp_checkbox, self.window().watchfolder_enabled_checkbox, self.window().allow_exit_node_checkbox, self.window().developer_mode_enabled_checkbox, self.window().checkbox_enable_network_statistics, self.window().checkbox_enable_resource_log, self.window().download_settings_add_to_channel_checkbox, ]: checkbox.setStyleSheet(checkbox_style) self.update_stacked_widget_height() def confirm_fully_empty_tokens(self): self.confirm_empty_tokens_dialog = ConfirmationDialog( self, "Empty tokens into another account", "Are you sure you want to empty ALL bandwidth tokens " "into another account? " "Warning: one-way action that cannot be revered", [('EMPTY', BUTTON_TYPE_CONFIRM), ('CANCEL', BUTTON_TYPE_NORMAL)], ) self.confirm_empty_tokens_dialog.button_clicked.connect(self.on_confirm_fully_empty_tokens) self.confirm_empty_tokens_dialog.show() def on_confirm_fully_empty_tokens(self, action): self.confirm_empty_tokens_dialog.close_dialog() self.confirm_empty_tokens_dialog = None if action == 0: TriblerNetworkRequest("trustchain/bootstrap", self.on_emptying_tokens) def partially_empty_tokens(self): self.empty_partial_tokens_dialog = ConfirmationDialog( self, "Empty tokens into another account", "Specify the amount of bandwidth tokens to empty into " "another account below:", [('EMPTY', BUTTON_TYPE_CONFIRM), ('CANCEL', BUTTON_TYPE_NORMAL)], show_input=True, ) self.empty_partial_tokens_dialog.dialog_widget.dialog_input.setPlaceholderText( 'Please enter the amount of tokens in MB' ) self.empty_partial_tokens_dialog.dialog_widget.dialog_input.setFocus() self.empty_partial_tokens_dialog.button_clicked.connect(self.confirm_partially_empty_tokens) self.empty_partial_tokens_dialog.show() def confirm_partially_empty_tokens(self, action): tokens = self.empty_partial_tokens_dialog.dialog_widget.dialog_input.text() self.empty_partial_tokens_dialog.close_dialog() self.empty_partial_tokens_dialog = None if action == 0: try: tokens = int(float(tokens)) except ValueError: ConfirmationDialog.show_error(self.window(), "Wrong input", "The provided amount is not a number") return self.confirm_empty_tokens_dialog = ConfirmationDialog( self, "Empty tokens into another account", "Are you sure you want to empty %d bandwidth tokens " "into another account? " "Warning: one-way action that cannot be revered" % tokens, [('EMPTY', BUTTON_TYPE_NORMAL), ('CANCEL', BUTTON_TYPE_CONFIRM)], ) self.confirm_empty_tokens_dialog.button_clicked.connect( lambda action2: self.on_confirm_partially_empty_tokens(action2, tokens) ) self.confirm_empty_tokens_dialog.show() def on_confirm_partially_empty_tokens(self, action, tokens): self.confirm_empty_tokens_dialog.close_dialog() self.confirm_empty_tokens_dialog = None if action == 0: TriblerNetworkRequest("trustchain/bootstrap?amount=%d" % (tokens * MEBIBYTE), self.on_emptying_tokens) def on_emptying_tokens(self, data): if not data: return json_data = json.dumps(data) if has_qr: self.empty_tokens_barcode_dialog = QWidget() self.empty_tokens_barcode_dialog.setWindowTitle("Please scan the following QR code") self.empty_tokens_barcode_dialog.setGeometry(10, 10, 500, 500) qr = qrcode.QRCode(version=1, error_correction=qrcode.constants.ERROR_CORRECT_M, box_size=10, border=5) qr.add_data(json_data) qr.make(fit=True) img = qr.make_image() # PIL format qim = ImageQt(img) pixmap = QtGui.QPixmap.fromImage(qim).scaled(600, 600, QtCore.Qt.KeepAspectRatio) label = QLabel(self.empty_tokens_barcode_dialog) label.setPixmap(pixmap) self.empty_tokens_barcode_dialog.resize(pixmap.width(), pixmap.height()) self.empty_tokens_barcode_dialog.show() else: ConfirmationDialog.show_error(self.window(), DEPENDENCY_ERROR_TITLE, DEPENDENCY_ERROR_MESSAGE) def on_channel_autocommit_checkbox_changed(self, _): self.window().gui_settings.setValue("autocommit_enabled", self.window().channel_autocommit_checkbox.isChecked()) def on_family_filter_checkbox_changed(self, _): self.window().gui_settings.setValue("family_filter", self.window().family_filter_checkbox.isChecked()) def on_developer_mode_checkbox_changed(self, _): self.window().gui_settings.setValue("debug", self.window().developer_mode_enabled_checkbox.isChecked()) self.window().left_menu_button_debug.setHidden(not self.window().developer_mode_enabled_checkbox.isChecked()) def on_use_monochrome_icon_checkbox_changed(self, _): use_monochrome_icon = self.window().use_monochrome_icon_checkbox.isChecked() self.window().gui_settings.setValue("use_monochrome_icon", use_monochrome_icon) self.window().update_tray_icon(use_monochrome_icon) def on_anon_download_state_changed(self, _): if self.window().download_settings_anon_checkbox.isChecked(): self.window().download_settings_anon_seeding_checkbox.setChecked(True) self.window().download_settings_anon_seeding_checkbox.setEnabled( not self.window().download_settings_anon_checkbox.isChecked() ) def on_choose_download_dir_clicked(self): previous_download_path = self.window().download_location_input.text() or "" download_dir = QFileDialog.getExistingDirectory( self.window(), "Please select the download location", previous_download_path, QFileDialog.ShowDirsOnly ) if not download_dir: return self.window().download_location_input.setText(download_dir) def on_choose_watch_dir_clicked(self): if self.window().watchfolder_enabled_checkbox.isChecked(): previous_watch_dir = self.window().watchfolder_location_input.text() or "" watch_dir = QFileDialog.getExistingDirectory( self.window(), "Please select the watch folder", previous_watch_dir, QFileDialog.ShowDirsOnly ) if not watch_dir: return self.window().watchfolder_location_input.setText(watch_dir) def on_choose_log_dir_clicked(self): previous_log_dir = self.window().log_location_input.text() or "" log_dir = QFileDialog.getExistingDirectory( self.window(), "Please select the log directory", previous_log_dir, QFileDialog.ShowDirsOnly ) if not log_dir or log_dir == previous_log_dir: return is_writable, error = is_dir_writable(log_dir) if not is_writable: gui_error_message = "<i>%s</i> is not writable. [%s]" % (log_dir, error) ConfirmationDialog.show_message(self.window(), "Insufficient Permissions", gui_error_message, "OK") else: self.window().log_location_input.setText(log_dir) def initialize_with_settings(self, settings): if not settings: return self.settings = settings settings = settings["settings"] gui_settings = self.window().gui_settings self.window().settings_stacked_widget.show() self.window().settings_tab.show() self.window().settings_save_button.show() # General settings self.window().family_filter_checkbox.setChecked( get_gui_setting(gui_settings, 'family_filter', True, is_bool=True) ) self.window().use_monochrome_icon_checkbox.setChecked( get_gui_setting(gui_settings, "use_monochrome_icon", False, is_bool=True) ) self.window().download_location_input.setText(settings['download_defaults']['saveas']) self.window().always_ask_location_checkbox.setChecked( get_gui_setting(gui_settings, "ask_download_settings", True, is_bool=True) ) self.window().download_settings_anon_checkbox.setChecked(settings['download_defaults']['anonymity_enabled']) self.window().download_settings_anon_seeding_checkbox.setChecked( settings['download_defaults']['safeseeding_enabled'] ) self.window().download_settings_add_to_channel_checkbox.setChecked( settings['download_defaults']['add_download_to_channel'] ) self.window().watchfolder_enabled_checkbox.setChecked(settings['watch_folder']['enabled']) self.window().watchfolder_location_input.setText(settings['watch_folder']['directory']) # Channel settings self.window().channel_autocommit_checkbox.setChecked( get_gui_setting(gui_settings, "autocommit_enabled", True, is_bool=True) ) # Market settings self.window().enable_market_checkbox.setChecked(settings['market_community']['enabled']) # Log directory self.window().log_location_input.setText(settings['general']['log_dir']) # Connection settings self.window().lt_proxy_type_combobox.setCurrentIndex(settings['libtorrent']['proxy_type']) if settings['libtorrent']['proxy_server']: proxy_server = settings['libtorrent']['proxy_server'].split(":") self.window().lt_proxy_server_input.setText(proxy_server[0]) self.window().lt_proxy_port_input.setText(proxy_server[1]) if settings['libtorrent']['proxy_auth']: proxy_auth = settings['libtorrent']['proxy_auth'].split(":") self.window().lt_proxy_username_input.setText(proxy_auth[0]) self.window().lt_proxy_password_input.setText(proxy_auth[1]) self.window().lt_utp_checkbox.setChecked(settings['libtorrent']['utp']) max_conn_download = settings['libtorrent']['max_connections_download'] if max_conn_download == -1: max_conn_download = 0 self.window().max_connections_download_input.setText(str(max_conn_download)) self.window().api_port_input.setText("%s" % get_gui_setting(gui_settings, "api_port", DEFAULT_API_PORT)) # Bandwidth settings self.window().upload_rate_limit_input.setText(str(settings['libtorrent']['max_upload_rate'] // 1024)) self.window().download_rate_limit_input.setText(str(settings['libtorrent']['max_download_rate'] // 1024)) # Seeding settings getattr(self.window(), "seeding_" + settings['download_defaults']['seeding_mode'] + "_radio").setChecked(True) self.window().seeding_time_input.setText(seconds_to_hhmm_string(settings['download_defaults']['seeding_time'])) ind = self.window().seeding_ratio_combobox.findText(str(settings['download_defaults']['seeding_ratio'])) if ind != -1: self.window().seeding_ratio_combobox.setCurrentIndex(ind) # Anonymity settings self.window().allow_exit_node_checkbox.setChecked(settings['tunnel_community']['exitnode_enabled']) self.window().number_hops_slider.setValue(int(settings['download_defaults']['number_hops'])) self.window().number_hops_slider.valueChanged.connect(self.update_anonymity_cost_label) self.update_anonymity_cost_label(int(settings['download_defaults']['number_hops'])) # Debug self.window().developer_mode_enabled_checkbox.setChecked( get_gui_setting(gui_settings, "debug", False, is_bool=True) ) self.window().checkbox_enable_resource_log.setChecked(settings['resource_monitor']['enabled']) cpu_priority = 1 if 'cpu_priority' in settings['resource_monitor']: cpu_priority = int(settings['resource_monitor']['cpu_priority']) self.window().slider_cpu_level.setValue(cpu_priority) self.window().cpu_priority_value.setText("Current Priority = %s" % cpu_priority) self.window().slider_cpu_level.valueChanged.connect(self.show_updated_cpu_priority) self.window().checkbox_enable_network_statistics.setChecked(settings['ipv8']['statistics']) def update_anonymity_cost_label(self, value): html_text = """<html><head/><body><p>Download with <b>%d</b> hop(s) of anonymity. When you download a file of 200 Megabyte, you will pay roughly <b>%d</b> Megabyte of bandwidth tokens.</p></body></html> """ % ( value, 400 * (value - 1) + 200, ) self.window().anonymity_costs_label.setText(html_text) def show_updated_cpu_priority(self, value): self.window().cpu_priority_value.setText("Current Priority = %s" % value) def load_settings(self): self.window().settings_stacked_widget.hide() self.window().settings_tab.hide() self.window().settings_save_button.hide() TriblerNetworkRequest("settings", self.initialize_with_settings) def clicked_tab_button(self, tab_button_name): if tab_button_name == "settings_general_button": self.window().settings_stacked_widget.setCurrentIndex(PAGE_SETTINGS_GENERAL) elif tab_button_name == "settings_connection_button": self.window().settings_stacked_widget.setCurrentIndex(PAGE_SETTINGS_CONNECTION) elif tab_button_name == "settings_bandwidth_button": self.window().settings_stacked_widget.setCurrentIndex(PAGE_SETTINGS_BANDWIDTH) elif tab_button_name == "settings_seeding_button": self.window().settings_stacked_widget.setCurrentIndex(PAGE_SETTINGS_SEEDING) elif tab_button_name == "settings_anonymity_button": self.window().settings_stacked_widget.setCurrentIndex(PAGE_SETTINGS_ANONYMITY) elif tab_button_name == "settings_debug_button": self.window().settings_stacked_widget.setCurrentIndex(PAGE_SETTINGS_DEBUG) self.update_stacked_widget_height() def update_stacked_widget_height(self): """ Update the height of the settings tab. This is required since the height of a QStackedWidget is by default the height of the largest page. This messes up the scroll bar. """ for index in range(self.window().settings_stacked_widget.count()): if index == self.window().settings_stacked_widget.currentIndex(): self.window().settings_stacked_widget.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) else: self.window().settings_stacked_widget.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored) self.window().settings_stacked_widget.adjustSize() def save_settings(self): # Create a dictionary with all available settings settings_data = { 'general': {}, 'Tribler': {}, 'download_defaults': {}, 'libtorrent': {}, 'watch_folder': {}, 'tunnel_community': {}, 'market_community': {}, 'trustchain': {}, 'resource_monitor': {}, 'ipv8': {}, 'chant': {}, } settings_data['download_defaults']['saveas'] = self.window().download_location_input.text() settings_data['general']['log_dir'] = self.window().log_location_input.text() settings_data['watch_folder']['enabled'] = self.window().watchfolder_enabled_checkbox.isChecked() if settings_data['watch_folder']['enabled']: settings_data['watch_folder']['directory'] = self.window().watchfolder_location_input.text() settings_data['market_community']['enabled'] = self.window().enable_market_checkbox.isChecked() settings_data['libtorrent']['proxy_type'] = self.window().lt_proxy_type_combobox.currentIndex() if ( self.window().lt_proxy_server_input.text() and len(self.window().lt_proxy_server_input.text()) > 0 and len(self.window().lt_proxy_port_input.text()) > 0 ): try: settings_data['libtorrent']['proxy_server'] = "%s:%s" % ( self.window().lt_proxy_server_input.text(), int(self.window().lt_proxy_port_input.text()), ) except ValueError: ConfirmationDialog.show_error( self.window(), "Invalid proxy port number", "You've entered an invalid format for the proxy port number. " "Please enter a whole number.", ) return else: settings_data['libtorrent']['proxy_server'] = ":" if self.window().lt_proxy_username_input.text() and self.window().lt_proxy_password_input.text(): settings_data['libtorrent']['proxy_auth'] = "%s:%s" % ( self.window().lt_proxy_username_input.text(), self.window().lt_proxy_password_input.text(), ) else: settings_data['libtorrent']['proxy_auth'] = ":" settings_data['libtorrent']['utp'] = self.window().lt_utp_checkbox.isChecked() try: max_conn_download = int(self.window().max_connections_download_input.text()) except ValueError: ConfirmationDialog.show_error( self.window(), "Invalid number of connections", "You've entered an invalid format for the maximum number of connections. " "Please enter a whole number.", ) return if max_conn_download == 0: max_conn_download = -1 settings_data['libtorrent']['max_connections_download'] = max_conn_download try: if self.window().upload_rate_limit_input.text(): user_upload_rate_limit = int(self.window().upload_rate_limit_input.text()) * 1024 if user_upload_rate_limit < sys.maxsize: settings_data['libtorrent']['max_upload_rate'] = user_upload_rate_limit else: raise ValueError if self.window().download_rate_limit_input.text(): user_download_rate_limit = int(self.window().download_rate_limit_input.text()) * 1024 if user_download_rate_limit < sys.maxsize: settings_data['libtorrent']['max_download_rate'] = user_download_rate_limit else: raise ValueError except ValueError: ConfirmationDialog.show_error( self.window(), "Invalid value for bandwidth limit", "You've entered an invalid value for the maximum upload/download rate. " "Please enter a whole number (max: %d)" % (sys.maxsize / 1000), ) return try: if self.window().api_port_input.text(): api_port = int(self.window().api_port_input.text()) if api_port <= 0 or api_port >= 65536: raise ValueError() self.window().gui_settings.setValue("api_port", api_port) except ValueError: ConfirmationDialog.show_error( self.window(), "Invalid value for api port", "Please enter a valid port for the api (between 0 and 65536)", ) return seeding_modes = ['forever', 'time', 'never', 'ratio'] selected_mode = 'forever' for seeding_mode in seeding_modes: if getattr(self.window(), "seeding_" + seeding_mode + "_radio").isChecked(): selected_mode = seeding_mode break settings_data['download_defaults']['seeding_mode'] = selected_mode settings_data['download_defaults']['seeding_ratio'] = float(self.window().seeding_ratio_combobox.currentText()) try: settings_data['download_defaults']['seeding_time'] = string_to_seconds( self.window().seeding_time_input.text() ) except ValueError: ConfirmationDialog.show_error( self.window(), "Invalid seeding time", "You've entered an invalid format for the seeding time (expected HH:MM)", ) return settings_data['tunnel_community']['exitnode_enabled'] = self.window().allow_exit_node_checkbox.isChecked() settings_data['download_defaults']['number_hops'] = self.window().number_hops_slider.value() settings_data['download_defaults'][ 'anonymity_enabled' ] = self.window().download_settings_anon_checkbox.isChecked() settings_data['download_defaults'][ 'safeseeding_enabled' ] = self.window().download_settings_anon_seeding_checkbox.isChecked() settings_data['download_defaults'][ 'add_download_to_channel' ] = self.window().download_settings_add_to_channel_checkbox.isChecked() settings_data['resource_monitor']['enabled'] = self.window().checkbox_enable_resource_log.isChecked() settings_data['resource_monitor']['cpu_priority'] = int(self.window().slider_cpu_level.value()) # network statistics settings_data['ipv8']['statistics'] = self.window().checkbox_enable_network_statistics.isChecked() self.window().settings_save_button.setEnabled(False) TriblerNetworkRequest("settings", self.on_settings_saved, method='POST', raw_data=json.dumps(settings_data)) def on_settings_saved(self, data): if not data: return # Now save the GUI settings self.window().gui_settings.setValue("family_filter", self.window().family_filter_checkbox.isChecked()) self.window().gui_settings.setValue("autocommit_enabled", self.window().channel_autocommit_checkbox.isChecked()) self.window().gui_settings.setValue( "ask_download_settings", self.window().always_ask_location_checkbox.isChecked() ) self.window().gui_settings.setValue( "use_monochrome_icon", self.window().use_monochrome_icon_checkbox.isChecked() ) self.saved_dialog = ConfirmationDialog( TriblerRequestManager.window, "Settings saved", "Your settings have been saved.", [('CLOSE', BUTTON_TYPE_NORMAL)], ) self.saved_dialog.button_clicked.connect(self.on_dialog_cancel_clicked) self.saved_dialog.show() self.window().fetch_settings() def on_dialog_cancel_clicked(self, _): self.window().settings_save_button.setEnabled(True) self.saved_dialog.close_dialog() self.saved_dialog = None
class DownloadsPage(QWidget): """ This class is responsible for managing all items on the downloads page. The downloads page shows all downloads and specific details about a download. """ received_downloads = pyqtSignal(object) def __init__(self): QWidget.__init__(self) self.export_dir = None self.filter = DOWNLOADS_FILTER_ALL self.download_widgets = {} # key: infohash, value: QTreeWidgetItem self.downloads = None self.downloads_timer = QTimer() self.downloads_timeout_timer = QTimer() self.downloads_last_update = 0 self.selected_items = [] self.dialog = None self.loading_message_widget = None self.total_download = 0 self.total_upload = 0 self.rest_request = None def showEvent(self, QShowEvent): """ When the downloads tab is clicked, we want to update the downloads list immediately. """ super(DownloadsPage, self).showEvent(QShowEvent) self.stop_loading_downloads() self.schedule_downloads_timer(True) def initialize_downloads_page(self): self.window().downloads_tab.initialize() self.window().downloads_tab.clicked_tab_button.connect( self.on_downloads_tab_button_clicked) self.window().start_download_button.clicked.connect( self.on_start_download_clicked) self.window().stop_download_button.clicked.connect( self.on_stop_download_clicked) self.window().remove_download_button.clicked.connect( self.on_remove_download_clicked) self.window().play_download_button.clicked.connect( self.on_play_download_clicked) self.window().downloads_list.itemSelectionChanged.connect( self.on_download_item_clicked) self.window().downloads_list.customContextMenuRequested.connect( self.on_right_click_item) self.window().download_details_widget.initialize_details_widget() self.window().download_details_widget.hide() self.window().downloads_filter_input.textChanged.connect( self.on_filter_text_changed) self.window().downloads_list.header().setSortIndicator( 12, Qt.AscendingOrder) self.window().downloads_list.header().resizeSection(12, 146) if not self.window().vlc_available: self.window().play_download_button.setHidden(True) def on_filter_text_changed(self, text): self.window().downloads_list.clearSelection() self.window().download_details_widget.hide() self.update_download_visibility() def start_loading_downloads(self): self.window().downloads_list.setSelectionMode( QAbstractItemView.NoSelection) self.loading_message_widget = QTreeWidgetItem() self.window().downloads_list.addTopLevelItem( self.loading_message_widget) self.window().downloads_list.setItemWidget( self.loading_message_widget, 2, LoadingListItem(self.window().downloads_list)) self.schedule_downloads_timer(now=True) def schedule_downloads_timer(self, now=False): self.downloads_timer = QTimer() self.downloads_timer.setSingleShot(True) self.downloads_timer.timeout.connect(self.load_downloads) self.downloads_timer.start(0 if now else 1000) self.downloads_timeout_timer = QTimer() self.downloads_timeout_timer.setSingleShot(True) self.downloads_timeout_timer.timeout.connect( self.on_downloads_request_timeout) self.downloads_timeout_timer.start(16000) def on_downloads_request_timeout(self): if self.rest_request: self.rest_request.cancel_request() self.schedule_downloads_timer() def stop_loading_downloads(self): self.downloads_timer.stop() self.downloads_timeout_timer.stop() def load_downloads(self): url = "downloads?get_pieces=1" if self.window().download_details_widget.currentIndex() == 3: url += "&get_peers=1" elif self.window().download_details_widget.currentIndex() == 1: url += "&get_files=1" isactive = not self.isHidden() or self.window( ).video_player_page.needsupdate if isactive or (time.time() - self.downloads_last_update > 30): # Update if the downloads page is visible or if we haven't updated for longer than 30 seconds self.downloads_last_update = time.time() priority = QNetworkRequest.LowPriority if not isactive else QNetworkRequest.HighPriority if self.rest_request: self.rest_request.cancel_request() self.rest_request = TriblerNetworkRequest( url, self.on_received_downloads, priority=priority) def on_received_downloads(self, downloads): if not downloads or "downloads" not in downloads: return # This might happen when closing Tribler loading_widget_index = self.window( ).downloads_list.indexOfTopLevelItem(self.loading_message_widget) if loading_widget_index > -1: self.window().downloads_list.takeTopLevelItem(loading_widget_index) self.window().downloads_list.setSelectionMode( QAbstractItemView.ExtendedSelection) self.downloads = downloads self.total_download = 0 self.total_upload = 0 download_infohashes = set() items = [] for download in downloads["downloads"]: if download["infohash"] in self.download_widgets: item = self.download_widgets[download["infohash"]] else: item = DownloadWidgetItem() self.download_widgets[download["infohash"]] = item items.append(item) item.update_with_download(download) # Update video player with download info video_infohash = self.window().video_player_page.active_infohash if video_infohash != "" and download["infohash"] == video_infohash: self.window().video_player_page.update_with_download_info( download) self.total_download += download["speed_down"] self.total_upload += download["speed_up"] download_infohashes.add(download["infohash"]) if (self.window().download_details_widget.current_download is not None and self.window().download_details_widget. current_download["infohash"] == download["infohash"]): self.window( ).download_details_widget.current_download = download self.window().download_details_widget.update_pages() self.window().downloads_list.addTopLevelItems(items) for item in items: self.window().downloads_list.setItemWidget(item, 2, item.bar_container) # Check whether there are download that should be removed for infohash, item in list(self.download_widgets.items()): if infohash not in download_infohashes: index = self.window().downloads_list.indexOfTopLevelItem(item) self.window().downloads_list.takeTopLevelItem(index) del self.download_widgets[infohash] self.window().tray_set_tooltip("Down: %s, Up: %s" % (format_speed( self.total_download), format_speed(self.total_upload))) self.update_download_visibility() self.schedule_downloads_timer() # Update the top download management button if we have a row selected if len(self.window().downloads_list.selectedItems()) > 0: self.on_download_item_clicked() self.received_downloads.emit(downloads) def update_download_visibility(self): for i in range(self.window().downloads_list.topLevelItemCount()): item = self.window().downloads_list.topLevelItem(i) if not isinstance(item, DownloadWidgetItem): continue filter_match = self.window().downloads_filter_input.text().lower( ) in item.download_info["name"].lower() is_channel = item.download_info["channel_download"] if self.filter == DOWNLOADS_FILTER_CHANNELS: item.setHidden(not is_channel or not filter_match) else: item.setHidden(not item.get_raw_download_status() in DOWNLOADS_FILTER_DEFINITION[self.filter] or not filter_match or is_channel) def on_downloads_tab_button_clicked(self, button_name): self.filter = button_name2filter[button_name] self.window().downloads_list.clearSelection() self.window().download_details_widget.hide() self.update_download_visibility() @staticmethod def start_download_enabled(download_widgets): return any([ download_widget.get_raw_download_status() == DLSTATUS_STOPPED for download_widget in download_widgets ]) @staticmethod def stop_download_enabled(download_widgets): return any([ download_widget.get_raw_download_status() not in [DLSTATUS_STOPPED, DLSTATUS_STOPPED_ON_ERROR] for download_widget in download_widgets ]) @staticmethod def force_recheck_download_enabled(download_widgets): return any([ download_widget.get_raw_download_status() not in [ DLSTATUS_METADATA, DLSTATUS_HASHCHECKING, DLSTATUS_WAITING4HASHCHECK ] for download_widget in download_widgets ]) def on_download_item_clicked(self): selected_count = len(self.window().downloads_list.selectedItems()) if selected_count == 0: self.window().play_download_button.setEnabled(False) self.window().remove_download_button.setEnabled(False) self.window().start_download_button.setEnabled(False) self.window().stop_download_button.setEnabled(False) self.window().download_details_widget.hide() elif selected_count == 1: self.selected_items = self.window().downloads_list.selectedItems() self.window().play_download_button.setEnabled(True) self.window().remove_download_button.setEnabled(True) self.window().start_download_button.setEnabled( DownloadsPage.start_download_enabled(self.selected_items)) self.window().stop_download_button.setEnabled( DownloadsPage.stop_download_enabled(self.selected_items)) self.window().download_details_widget.update_with_download( self.selected_items[0].download_info) self.window().download_details_widget.show() else: self.selected_items = self.window().downloads_list.selectedItems() self.window().play_download_button.setEnabled(False) self.window().remove_download_button.setEnabled(True) self.window().start_download_button.setEnabled( DownloadsPage.start_download_enabled(self.selected_items)) self.window().stop_download_button.setEnabled( DownloadsPage.stop_download_enabled(self.selected_items)) self.window().download_details_widget.hide() def on_start_download_clicked(self): for selected_item in self.selected_items: infohash = selected_item.download_info["infohash"] TriblerNetworkRequest("downloads/%s" % infohash, self.on_download_resumed, method='PATCH', data={"state": "resume"}) def on_download_resumed(self, json_result): if json_result and 'modified' in json_result: for selected_item in self.selected_items: if selected_item.download_info["infohash"] == json_result[ "infohash"]: selected_item.download_info[ 'status'] = "DLSTATUS_DOWNLOADING" selected_item.update_item() self.on_download_item_clicked() def on_stop_download_clicked(self): for selected_item in self.selected_items: infohash = selected_item.download_info["infohash"] TriblerNetworkRequest("downloads/%s" % infohash, self.on_download_stopped, method='PATCH', data={"state": "stop"}) def on_play_download_clicked(self): self.window().left_menu_button_video_player.click() selected_item = self.selected_items[:1] if selected_item: self.window().video_player_page.play_media_item( selected_item[0].download_info["infohash"], -1) def on_download_stopped(self, json_result): if json_result and "modified" in json_result: for selected_item in self.selected_items: if selected_item.download_info["infohash"] == json_result[ "infohash"]: selected_item.download_info['status'] = "DLSTATUS_STOPPED" selected_item.update_item() self.on_download_item_clicked() if self.window( ).video_player_page.active_infohash == selected_item.download_info[ "infohash"]: self.window().video_player_page.reset_player() def on_remove_download_clicked(self): self.dialog = ConfirmationDialog( self, "Remove download", "Are you sure you want to remove this download?", [ ('remove download', BUTTON_TYPE_NORMAL), ('remove download + data', BUTTON_TYPE_NORMAL), ('cancel', BUTTON_TYPE_CONFIRM), ], ) self.dialog.button_clicked.connect(self.on_remove_download_dialog) self.dialog.show() def on_remove_download_dialog(self, action): if action != 2: for selected_item in self.selected_items: infohash = selected_item.download_info["infohash"] # Reset video player if necessary before doing the actual request if self.window().video_player_page.active_infohash == infohash: self.window().video_player_page.reset_player() TriblerNetworkRequest( "downloads/%s" % infohash, self.on_download_removed, method='DELETE', data={"remove_data": bool(action)}, ) if self.dialog: self.dialog.close_dialog() self.dialog = None def on_download_removed(self, json_result): if json_result and "removed" in json_result: self.load_downloads() self.window().download_details_widget.hide() def on_force_recheck_download(self): for selected_item in self.selected_items: infohash = selected_item.download_info["infohash"] TriblerNetworkRequest("downloads/%s" % infohash, self.on_forced_recheck, method='PATCH', data={"state": "recheck"}) def on_forced_recheck(self, result): if result and "modified" in result: for selected_item in self.selected_items: if selected_item.download_info["infohash"] == result[ "infohash"]: selected_item.download_info[ 'status'] = "DLSTATUS_HASHCHECKING" selected_item.update_item() self.on_download_item_clicked() def on_change_anonymity(self, result): if result and "modified" in result: if result["infohash"] == self.window( ).video_player_page.active_infohash: self.window().video_player_page.reset_player() def change_anonymity(self, hops): for selected_item in self.selected_items: infohash = selected_item.download_info["infohash"] TriblerNetworkRequest("downloads/%s" % infohash, self.on_change_anonymity, method='PATCH', data={"anon_hops": hops}) def on_explore_files(self): for selected_item in self.selected_items: path = os.path.normpath(selected_item.download_info["destination"]) QDesktopServices.openUrl(QUrl.fromLocalFile(path)) def on_move_files(self): if len(self.selected_items) != 1: return dest_dir = QFileDialog.getExistingDirectory( self, "Please select the destination directory", self.selected_items[0].download_info["destination"], QFileDialog.ShowDirsOnly, ) if not dest_dir: return _infohash = self.selected_items[0].download_info["infohash"] _name = self.selected_items[0].download_info["name"] data = {"state": "move_storage", "dest_dir": dest_dir} TriblerNetworkRequest( "downloads/%s" % _infohash, lambda res, _, name=_name, target=dest_dir: self.on_files_moved( res, name, target), data=data, method='PATCH', ) def on_files_moved(self, response, name, dest_dir): if "modified" in response and response["modified"]: self.window().tray_show_message(name, "Moved to %s" % dest_dir) def on_export_download(self): self.export_dir = QFileDialog.getExistingDirectory( self, "Please select the destination directory", "", QFileDialog.ShowDirsOnly) selected_item = self.selected_items[:1] if len(self.export_dir) > 0 and selected_item: # Show confirmation dialog where we specify the name of the file torrent_name = selected_item[0].download_info['name'] self.dialog = ConfirmationDialog( self, "Export torrent file", "Please enter the name of the torrent file:", [('SAVE', BUTTON_TYPE_NORMAL), ('CANCEL', BUTTON_TYPE_CONFIRM)], show_input=True, ) self.dialog.dialog_widget.dialog_input.setPlaceholderText( 'Torrent file name') self.dialog.dialog_widget.dialog_input.setText("%s.torrent" % torrent_name) self.dialog.dialog_widget.dialog_input.setFocus() self.dialog.button_clicked.connect( self.on_export_download_dialog_done) self.dialog.show() def on_export_download_dialog_done(self, action): selected_item = self.selected_items[:1] if action == 0 and selected_item: filename = self.dialog.dialog_widget.dialog_input.text() TriblerFileDownloadRequest( "downloads/%s/torrent" % selected_item[0].download_info['infohash'], lambda data, _: self.on_export_download_request_done( filename, data), ) self.dialog.close_dialog() self.dialog = None def on_export_download_request_done(self, filename, data): dest_path = os.path.join(self.export_dir, filename) try: torrent_file = open(dest_path, "wb") torrent_file.write(data) torrent_file.close() except IOError as exc: ConfirmationDialog.show_error( self.window(), "Error when exporting file", "An error occurred when exporting the torrent file: %s" % str(exc), ) else: self.window().tray_show_message( "Torrent file exported", "Torrent file exported to %s" % dest_path) def on_add_to_channel(self): def on_add_button_pressed(channel_id): for selected_item in self.selected_items: infohash = selected_item.download_info["infohash"] name = selected_item.download_info["name"] TriblerNetworkRequest( f"channels/mychannel/{channel_id}/torrents", lambda _: self.window().tray_show_message( "Channel update", "Torrent(s) added to your channel"), method='PUT', data={"uri": compose_magnetlink(infohash, name=name)}, ) self.window().add_to_channel_dialog.show_dialog( on_add_button_pressed, confirm_button_text="Add torrent(s)") def on_right_click_item(self, pos): item_clicked = self.window().downloads_list.itemAt(pos) if not item_clicked or self.selected_items is None: return if item_clicked not in self.selected_items: self.selected_items.append(item_clicked) menu = TriblerActionMenu(self) start_action = QAction('Start', self) stop_action = QAction('Stop', self) remove_download_action = QAction('Remove download', self) add_to_channel_action = QAction('Add to my channel', self) force_recheck_action = QAction('Force recheck', self) export_download_action = QAction('Export .torrent file', self) explore_files_action = QAction('Explore files', self) move_files_action = QAction('Move file storage', self) no_anon_action = QAction('No anonymity', self) one_hop_anon_action = QAction('One hop', self) two_hop_anon_action = QAction('Two hops', self) three_hop_anon_action = QAction('Three hops', self) start_action.triggered.connect(self.on_start_download_clicked) start_action.setEnabled( DownloadsPage.start_download_enabled(self.selected_items)) stop_action.triggered.connect(self.on_stop_download_clicked) stop_action.setEnabled( DownloadsPage.stop_download_enabled(self.selected_items)) add_to_channel_action.triggered.connect(self.on_add_to_channel) remove_download_action.triggered.connect( self.on_remove_download_clicked) force_recheck_action.triggered.connect(self.on_force_recheck_download) force_recheck_action.setEnabled( DownloadsPage.force_recheck_download_enabled(self.selected_items)) export_download_action.triggered.connect(self.on_export_download) explore_files_action.triggered.connect(self.on_explore_files) move_files_action.triggered.connect(self.on_move_files) no_anon_action.triggered.connect(lambda: self.change_anonymity(0)) one_hop_anon_action.triggered.connect(lambda: self.change_anonymity(1)) two_hop_anon_action.triggered.connect(lambda: self.change_anonymity(2)) three_hop_anon_action.triggered.connect( lambda: self.change_anonymity(3)) menu.addAction(start_action) menu.addAction(stop_action) if self.window().vlc_available and len(self.selected_items) == 1: play_action = QAction('Play', self) play_action.triggered.connect(self.on_play_download_clicked) menu.addAction(play_action) menu.addSeparator() menu.addAction(add_to_channel_action) menu.addSeparator() menu.addAction(remove_download_action) menu.addSeparator() menu.addAction(force_recheck_action) menu.addSeparator() exclude_states = [ DLSTATUS_METADATA, DLSTATUS_CIRCUITS, DLSTATUS_EXIT_NODES, DLSTATUS_HASHCHECKING, DLSTATUS_WAITING4HASHCHECK, ] if len(self.selected_items) == 1 and self.selected_items[ 0].get_raw_download_status() not in exclude_states: menu.addAction(export_download_action) menu.addAction(explore_files_action) if len(self.selected_items) == 1: menu.addAction(move_files_action) menu.addSeparator() menu_anon_level = menu.addMenu("Change Anonymity ") menu_anon_level.addAction(no_anon_action) menu_anon_level.addAction(one_hop_anon_action) menu_anon_level.addAction(two_hop_anon_action) menu_anon_level.addAction(three_hop_anon_action) menu.exec_(self.window().downloads_list.mapToGlobal(pos))
def save_settings(self, checked): # Create a dictionary with all available settings settings_data = { 'general': {}, 'Tribler': {}, 'download_defaults': {}, 'libtorrent': {}, 'watch_folder': {}, 'tunnel_community': {}, 'trustchain': {}, 'resource_monitor': {}, 'ipv8': {}, 'chant': {}, } settings_data['download_defaults']['saveas'] = self.window( ).download_location_input.text() settings_data['general']['log_dir'] = self.window( ).log_location_input.text() settings_data['watch_folder']['enabled'] = self.window( ).watchfolder_enabled_checkbox.isChecked() if settings_data['watch_folder']['enabled']: settings_data['watch_folder']['directory'] = self.window( ).watchfolder_location_input.text() settings_data['libtorrent']['proxy_type'] = self.window( ).lt_proxy_type_combobox.currentIndex() if (self.window().lt_proxy_server_input.text() and len(self.window().lt_proxy_server_input.text()) > 0 and len(self.window().lt_proxy_port_input.text()) > 0): try: settings_data['libtorrent']['proxy_server'] = "{}:{}".format( self.window().lt_proxy_server_input.text(), int(self.window().lt_proxy_port_input.text()), ) except ValueError: ConfirmationDialog.show_error( self.window(), tr("Invalid proxy port number"), tr("You've entered an invalid format for the proxy port number. Please enter a whole number." ), ) return else: settings_data['libtorrent']['proxy_server'] = ":" if self.window().lt_proxy_username_input.text() and self.window( ).lt_proxy_password_input.text(): settings_data['libtorrent']['proxy_auth'] = "{}:{}".format( self.window().lt_proxy_username_input.text(), self.window().lt_proxy_password_input.text(), ) else: settings_data['libtorrent']['proxy_auth'] = ":" settings_data['libtorrent']['utp'] = self.window( ).lt_utp_checkbox.isChecked() try: max_conn_download = int( self.window().max_connections_download_input.text()) except ValueError: ConfirmationDialog.show_error( self.window(), tr("Invalid number of connections"), tr("You've entered an invalid format for the maximum number of connections. " "Please enter a whole number."), ) return if max_conn_download == 0: max_conn_download = -1 settings_data['libtorrent'][ 'max_connections_download'] = max_conn_download try: if self.window().upload_rate_limit_input.text(): user_upload_rate_limit = int( float(self.window().upload_rate_limit_input.text()) * 1024) if user_upload_rate_limit < MAX_LIBTORRENT_RATE_LIMIT: settings_data['libtorrent'][ 'max_upload_rate'] = user_upload_rate_limit else: raise ValueError if self.window().download_rate_limit_input.text(): user_download_rate_limit = int( float(self.window().download_rate_limit_input.text()) * 1024) if user_download_rate_limit < MAX_LIBTORRENT_RATE_LIMIT: settings_data['libtorrent'][ 'max_download_rate'] = user_download_rate_limit else: raise ValueError except ValueError: ConfirmationDialog.show_error( self.window(), tr("Invalid value for bandwidth limit"), tr("You've entered an invalid value for the maximum upload/download rate. \n" "The rate is specified in KB/s and the value permitted is between 0 and %d KB/s.\n" "Note that the decimal values are truncated.") % (MAX_LIBTORRENT_RATE_LIMIT / 1024), ) return try: if self.window().api_port_input.text(): api_port = int(self.window().api_port_input.text()) if api_port <= 0 or api_port >= 65536: raise ValueError() self.window().gui_settings.setValue("api_port", api_port) except ValueError: ConfirmationDialog.show_error( self.window(), tr("Invalid value for api port"), tr("Please enter a valid port for the api (between 0 and 65536)" ), ) return seeding_modes = ['forever', 'time', 'never', 'ratio'] selected_mode = 'forever' for seeding_mode in seeding_modes: if getattr(self.window(), "seeding_" + seeding_mode + "_radio").isChecked(): selected_mode = seeding_mode break settings_data['download_defaults']['seeding_mode'] = selected_mode settings_data['download_defaults']['seeding_ratio'] = float( self.window().seeding_ratio_combobox.currentText()) try: settings_data['download_defaults'][ 'seeding_time'] = string_to_seconds( self.window().seeding_time_input.text()) except ValueError: ConfirmationDialog.show_error( self.window(), tr("Invalid seeding time"), tr("You've entered an invalid format for the seeding time (expected HH:MM)" ), ) return settings_data['tunnel_community']['exitnode_enabled'] = self.window( ).allow_exit_node_checkbox.isChecked() settings_data['download_defaults']['number_hops'] = self.window( ).number_hops_slider.value() settings_data['download_defaults']['anonymity_enabled'] = self.window( ).download_settings_anon_checkbox.isChecked() settings_data['download_defaults'][ 'safeseeding_enabled'] = self.window( ).download_settings_anon_seeding_checkbox.isChecked() settings_data['download_defaults'][ 'add_download_to_channel'] = self.window( ).download_settings_add_to_channel_checkbox.isChecked() settings_data['resource_monitor']['enabled'] = self.window( ).checkbox_enable_resource_log.isChecked() settings_data['resource_monitor']['cpu_priority'] = int( self.window().slider_cpu_level.value()) # network statistics settings_data['ipv8']['statistics'] = self.window( ).checkbox_enable_network_statistics.isChecked() self.window().settings_save_button.setEnabled(False) # TODO: do it in RESTful style, on the REST return JSON instead # In case the default save dir has changed, add it to the top of the list of last download locations. # Otherwise, the user could absentmindedly click through the download dialog and start downloading into # the last used download dir, and not into the newly designated default download dir. if self.settings['download_defaults']['saveas'] != settings_data[ 'download_defaults']['saveas']: self.window().update_recent_download_locations( settings_data['download_defaults']['saveas']) self.settings = settings_data TriblerNetworkRequest("settings", self.on_settings_saved, method='POST', raw_data=json.dumps(settings_data))
class SettingsPage(AddBreadcrumbOnShowMixin, QWidget): """ This class is responsible for displaying and adjusting the settings present in Tribler. """ def __init__(self): QWidget.__init__(self) self.settings = None self.saved_dialog = None self.version_history = VersionHistory(get_root_state_directory()) def initialize_settings_page(self): self.window().settings_tab.initialize() connect(self.window().settings_tab.clicked_tab_button, self.clicked_tab_button) connect(self.window().settings_save_button.clicked, self.save_settings) connect(self.window().download_location_chooser_button.clicked, self.on_choose_download_dir_clicked) connect(self.window().watch_folder_chooser_button.clicked, self.on_choose_watch_dir_clicked) connect(self.window().channel_autocommit_checkbox.stateChanged, self.on_channel_autocommit_checkbox_changed) connect(self.window().family_filter_checkbox.stateChanged, self.on_family_filter_checkbox_changed) connect(self.window().developer_mode_enabled_checkbox.stateChanged, self.on_developer_mode_checkbox_changed) connect(self.window().use_monochrome_icon_checkbox.stateChanged, self.on_use_monochrome_icon_checkbox_changed) connect(self.window().download_settings_anon_checkbox.stateChanged, self.on_anon_download_state_changed) connect(self.window().log_location_chooser_button.clicked, self.on_choose_log_dir_clicked) connect(self.window().btn_remove_old_state_dir.clicked, self.on_remove_version_dirs) checkbox_style = get_checkbox_style() for checkbox in [ self.window().family_filter_checkbox, self.window().channel_autocommit_checkbox, self.window().always_ask_location_checkbox, self.window().developer_mode_enabled_checkbox, self.window().use_monochrome_icon_checkbox, self.window().download_settings_anon_checkbox, self.window().download_settings_anon_seeding_checkbox, self.window().lt_utp_checkbox, self.window().watchfolder_enabled_checkbox, self.window().allow_exit_node_checkbox, self.window().developer_mode_enabled_checkbox, self.window().checkbox_enable_network_statistics, self.window().checkbox_enable_resource_log, self.window().download_settings_add_to_channel_checkbox, ]: checkbox.setStyleSheet(checkbox_style) self.update_stacked_widget_height() def on_channel_autocommit_checkbox_changed(self, _): self.window().gui_settings.setValue( "autocommit_enabled", self.window().channel_autocommit_checkbox.isChecked()) def on_family_filter_checkbox_changed(self, _): self.window().gui_settings.setValue( "family_filter", self.window().family_filter_checkbox.isChecked()) def on_developer_mode_checkbox_changed(self, _): self.window().gui_settings.setValue( "debug", self.window().developer_mode_enabled_checkbox.isChecked()) self.window().left_menu_button_debug.setHidden( not self.window().developer_mode_enabled_checkbox.isChecked()) def on_use_monochrome_icon_checkbox_changed(self, _): use_monochrome_icon = self.window( ).use_monochrome_icon_checkbox.isChecked() self.window().gui_settings.setValue("use_monochrome_icon", use_monochrome_icon) self.window().update_tray_icon(use_monochrome_icon) def on_anon_download_state_changed(self, _): if self.window().download_settings_anon_checkbox.isChecked(): self.window().download_settings_anon_seeding_checkbox.setChecked( True) self.window().download_settings_anon_seeding_checkbox.setEnabled( not self.window().download_settings_anon_checkbox.isChecked()) def on_choose_download_dir_clicked(self, checked): previous_download_path = self.window().download_location_input.text( ) or "" download_dir = QFileDialog.getExistingDirectory( self.window(), "Please select the download location", previous_download_path, QFileDialog.ShowDirsOnly) if not download_dir: return self.window().download_location_input.setText(download_dir) def on_choose_watch_dir_clicked(self, checked): if self.window().watchfolder_enabled_checkbox.isChecked(): previous_watch_dir = self.window().watchfolder_location_input.text( ) or "" watch_dir = QFileDialog.getExistingDirectory( self.window(), "Please select the watch folder", previous_watch_dir, QFileDialog.ShowDirsOnly) if not watch_dir: return self.window().watchfolder_location_input.setText(watch_dir) def on_choose_log_dir_clicked(self, checked): previous_log_dir = self.window().log_location_input.text() or "" log_dir = QFileDialog.getExistingDirectory( self.window(), tr("Please select the log directory"), previous_log_dir, QFileDialog.ShowDirsOnly) if not log_dir or log_dir == previous_log_dir: return is_writable, error = is_dir_writable(log_dir) if not is_writable: gui_error_message = f"<i>{log_dir}</i> is not writable. [{error}]" ConfirmationDialog.show_message(self.window(), tr("Insufficient Permissions"), gui_error_message, "OK") else: self.window().log_location_input.setText(log_dir) def initialize_with_settings(self, settings): if not settings: return self.settings = settings = settings["settings"] gui_settings = self.window().gui_settings self.window().settings_stacked_widget.show() self.window().settings_tab.show() self.window().settings_save_button.show() # General settings self.window().family_filter_checkbox.setChecked( get_gui_setting(gui_settings, 'family_filter', True, is_bool=True)) self.window().use_monochrome_icon_checkbox.setChecked( get_gui_setting(gui_settings, "use_monochrome_icon", False, is_bool=True)) self.window().download_location_input.setText( settings['download_defaults']['saveas']) self.window().always_ask_location_checkbox.setChecked( get_gui_setting(gui_settings, "ask_download_settings", True, is_bool=True)) self.window().download_settings_anon_checkbox.setChecked( settings['download_defaults']['anonymity_enabled']) self.window().download_settings_anon_seeding_checkbox.setChecked( settings['download_defaults']['safeseeding_enabled']) self.window().download_settings_add_to_channel_checkbox.setChecked( settings['download_defaults']['add_download_to_channel']) self.window().watchfolder_enabled_checkbox.setChecked( settings['watch_folder']['enabled']) self.window().watchfolder_location_input.setText( settings['watch_folder']['directory']) # Channel settings self.window().channel_autocommit_checkbox.setChecked( get_gui_setting(gui_settings, "autocommit_enabled", True, is_bool=True)) # Log directory self.window().log_location_input.setText( settings['general']['log_dir']) # Connection settings self.window().lt_proxy_type_combobox.setCurrentIndex( settings['libtorrent']['proxy_type']) if settings['libtorrent']['proxy_server']: proxy_server = settings['libtorrent']['proxy_server'].split(":") self.window().lt_proxy_server_input.setText(proxy_server[0]) self.window().lt_proxy_port_input.setText(proxy_server[1]) if settings['libtorrent']['proxy_auth']: proxy_auth = settings['libtorrent']['proxy_auth'].split(":") self.window().lt_proxy_username_input.setText(proxy_auth[0]) self.window().lt_proxy_password_input.setText(proxy_auth[1]) self.window().lt_utp_checkbox.setChecked(settings['libtorrent']['utp']) max_conn_download = settings['libtorrent']['max_connections_download'] if max_conn_download == -1: max_conn_download = 0 self.window().max_connections_download_input.setText( str(max_conn_download)) self.window().api_port_input.setText( f"{get_gui_setting(gui_settings, 'api_port', DEFAULT_API_PORT)}") # Bandwidth settings self.window().upload_rate_limit_input.setText( str(settings['libtorrent']['max_upload_rate'] // 1024)) self.window().download_rate_limit_input.setText( str(settings['libtorrent']['max_download_rate'] // 1024)) # Seeding settings getattr( self.window(), "seeding_" + settings['download_defaults']['seeding_mode'] + "_radio").setChecked(True) self.window().seeding_time_input.setText( seconds_to_hhmm_string( settings['download_defaults']['seeding_time'])) ind = self.window().seeding_ratio_combobox.findText( str(settings['download_defaults']['seeding_ratio'])) if ind != -1: self.window().seeding_ratio_combobox.setCurrentIndex(ind) # Anonymity settings self.window().allow_exit_node_checkbox.setChecked( settings['tunnel_community']['exitnode_enabled']) self.window().number_hops_slider.setValue( int(settings['download_defaults']['number_hops'])) connect(self.window().number_hops_slider.valueChanged, self.update_anonymity_cost_label) self.update_anonymity_cost_label( int(settings['download_defaults']['number_hops'])) # Data settings self.load_settings_data_tab() # Debug self.window().developer_mode_enabled_checkbox.setChecked( get_gui_setting(gui_settings, "debug", False, is_bool=True)) self.window().checkbox_enable_resource_log.setChecked( settings['resource_monitor']['enabled']) cpu_priority = 1 if 'cpu_priority' in settings['resource_monitor']: cpu_priority = int(settings['resource_monitor']['cpu_priority']) self.window().slider_cpu_level.setValue(cpu_priority) self.window().cpu_priority_value.setText( f"Current Priority = {cpu_priority}") connect(self.window().slider_cpu_level.valueChanged, self.show_updated_cpu_priority) self.window().checkbox_enable_network_statistics.setChecked( settings['ipv8']['statistics']) def _version_dir_checkbox(self, state_dir, enabled=True): dir_size = sum(f.stat().st_size for f in state_dir.glob('**/*')) text = f"{state_dir} {format_size(dir_size)}" checkbox = QCheckBox(text) checkbox.setEnabled(enabled) return checkbox def load_settings_data_tab(self): self.refresh_current_version_checkbox() self.refresh_old_version_checkboxes() def refresh_current_version_checkbox(self): get_root_state_directory() code_version_dir = self.version_history.code_version.directory self.refresh_version_checkboxes(self.window().state_dir_current, [code_version_dir], enabled=False) def refresh_old_version_checkboxes(self): get_root_state_directory() old_state_dirs = self.version_history.get_disposable_state_directories( ) self.refresh_version_checkboxes(self.window().state_dir_list, old_state_dirs, enabled=True) def refresh_version_checkboxes(self, parent, old_state_dirs, enabled=True): version_dirs_layout = parent.layout() for checkbox in parent.findChildren(QCheckBox): version_dirs_layout.removeWidget(checkbox) checkbox.setParent(None) checkbox.deleteLater() for state_dir in old_state_dirs: version_dir_checkbox = self._version_dir_checkbox(state_dir, enabled=enabled) version_dirs_layout.addWidget(version_dir_checkbox) def on_remove_version_dirs(self, _): root_version_dir = str(get_root_state_directory()) def dir_from_checkbox_text(checkbox): # eg text: "/home/<user>/.Tribler/v7.8 5 GB" state_dir = checkbox.text().rpartition(" ")[0] if not state_dir.startswith(root_version_dir): return None # safety check just for case state_dir = state_dir[len(root_version_dir):] if state_dir.startswith('/'): state_dir = state_dir[1:] return state_dir dirs_selected_for_deletion = [] for checkbox in self.window().state_dir_list.findChildren(QCheckBox): if checkbox.isChecked(): state_dir = dir_from_checkbox_text(checkbox) if state_dir: dirs_selected_for_deletion.append(state_dir) if self.on_confirm_remove_version_dirs(dirs_selected_for_deletion): remove_state_dirs(root_version_dir, dirs_selected_for_deletion) self.refresh_old_version_checkboxes() def on_confirm_remove_version_dirs(self, selected_versions): message_box = QMessageBox() message_box.setIcon(QMessageBox.Question) if selected_versions: version_dirs_str = "\n- ".join(selected_versions) versions_info = tr("Versions: \n- %s") % version_dirs_str title = tr("Confirm delete older versions?") message_body = (tr("Are you sure to remove the selected versions? " "\nYou can not undo this action." "\n\n ") % versions_info) message_buttons = QMessageBox.No | QMessageBox.Yes else: title = tr("No versions selected") message_body = tr("Select a version to delete.") message_buttons = QMessageBox.Close message_box.setWindowTitle(title) message_box.setText(message_body) message_box.setStandardButtons(message_buttons) user_choice = message_box.exec_() return user_choice == QMessageBox.Yes def update_anonymity_cost_label(self, value): html_text = tr( "<html><head/><body><p>Download with <b>%d</b> hop(s) of anonymity. " "When you download a file of 200 Megabyte, you will pay roughly <b>%d</b>" "Megabyte of bandwidth tokens.</p></body></html>") % ( value, 400 * (value - 1) + 200, ) self.window().anonymity_costs_label.setText(html_text) def show_updated_cpu_priority(self, value): self.window().cpu_priority_value.setText( tr("Current Priority = %s") % value) def load_settings(self): self.window().settings_stacked_widget.hide() self.window().settings_tab.hide() self.window().settings_save_button.hide() TriblerNetworkRequest("settings", self.initialize_with_settings) def clicked_tab_button(self, tab_button_name): if tab_button_name == "settings_general_button": self.window().settings_stacked_widget.setCurrentIndex( PAGE_SETTINGS_GENERAL) elif tab_button_name == "settings_connection_button": self.window().settings_stacked_widget.setCurrentIndex( PAGE_SETTINGS_CONNECTION) elif tab_button_name == "settings_bandwidth_button": self.window().settings_stacked_widget.setCurrentIndex( PAGE_SETTINGS_BANDWIDTH) elif tab_button_name == "settings_seeding_button": self.window().settings_stacked_widget.setCurrentIndex( PAGE_SETTINGS_SEEDING) elif tab_button_name == "settings_anonymity_button": self.window().settings_stacked_widget.setCurrentIndex( PAGE_SETTINGS_ANONYMITY) elif tab_button_name == "settings_data_button": self.window().settings_stacked_widget.setCurrentIndex( PAGE_SETTINGS_DATA) elif tab_button_name == "settings_debug_button": self.window().settings_stacked_widget.setCurrentIndex( PAGE_SETTINGS_DEBUG) self.update_stacked_widget_height() def update_stacked_widget_height(self): """ Update the height of the settings tab. This is required since the height of a QStackedWidget is by default the height of the largest page. This messes up the scroll bar. """ for index in range(self.window().settings_stacked_widget.count()): if index == self.window().settings_stacked_widget.currentIndex(): self.window().settings_stacked_widget.setSizePolicy( QSizePolicy.Preferred, QSizePolicy.Preferred) else: self.window().settings_stacked_widget.setSizePolicy( QSizePolicy.Ignored, QSizePolicy.Ignored) self.window().settings_stacked_widget.adjustSize() def save_settings(self, checked): # Create a dictionary with all available settings settings_data = { 'general': {}, 'Tribler': {}, 'download_defaults': {}, 'libtorrent': {}, 'watch_folder': {}, 'tunnel_community': {}, 'trustchain': {}, 'resource_monitor': {}, 'ipv8': {}, 'chant': {}, } settings_data['download_defaults']['saveas'] = self.window( ).download_location_input.text() settings_data['general']['log_dir'] = self.window( ).log_location_input.text() settings_data['watch_folder']['enabled'] = self.window( ).watchfolder_enabled_checkbox.isChecked() if settings_data['watch_folder']['enabled']: settings_data['watch_folder']['directory'] = self.window( ).watchfolder_location_input.text() settings_data['libtorrent']['proxy_type'] = self.window( ).lt_proxy_type_combobox.currentIndex() if (self.window().lt_proxy_server_input.text() and len(self.window().lt_proxy_server_input.text()) > 0 and len(self.window().lt_proxy_port_input.text()) > 0): try: settings_data['libtorrent']['proxy_server'] = "{}:{}".format( self.window().lt_proxy_server_input.text(), int(self.window().lt_proxy_port_input.text()), ) except ValueError: ConfirmationDialog.show_error( self.window(), tr("Invalid proxy port number"), tr("You've entered an invalid format for the proxy port number. Please enter a whole number." ), ) return else: settings_data['libtorrent']['proxy_server'] = ":" if self.window().lt_proxy_username_input.text() and self.window( ).lt_proxy_password_input.text(): settings_data['libtorrent']['proxy_auth'] = "{}:{}".format( self.window().lt_proxy_username_input.text(), self.window().lt_proxy_password_input.text(), ) else: settings_data['libtorrent']['proxy_auth'] = ":" settings_data['libtorrent']['utp'] = self.window( ).lt_utp_checkbox.isChecked() try: max_conn_download = int( self.window().max_connections_download_input.text()) except ValueError: ConfirmationDialog.show_error( self.window(), tr("Invalid number of connections"), tr("You've entered an invalid format for the maximum number of connections. " "Please enter a whole number."), ) return if max_conn_download == 0: max_conn_download = -1 settings_data['libtorrent'][ 'max_connections_download'] = max_conn_download try: if self.window().upload_rate_limit_input.text(): user_upload_rate_limit = int( float(self.window().upload_rate_limit_input.text()) * 1024) if user_upload_rate_limit < MAX_LIBTORRENT_RATE_LIMIT: settings_data['libtorrent'][ 'max_upload_rate'] = user_upload_rate_limit else: raise ValueError if self.window().download_rate_limit_input.text(): user_download_rate_limit = int( float(self.window().download_rate_limit_input.text()) * 1024) if user_download_rate_limit < MAX_LIBTORRENT_RATE_LIMIT: settings_data['libtorrent'][ 'max_download_rate'] = user_download_rate_limit else: raise ValueError except ValueError: ConfirmationDialog.show_error( self.window(), tr("Invalid value for bandwidth limit"), tr("You've entered an invalid value for the maximum upload/download rate. \n" "The rate is specified in KB/s and the value permitted is between 0 and %d KB/s.\n" "Note that the decimal values are truncated.") % (MAX_LIBTORRENT_RATE_LIMIT / 1024), ) return try: if self.window().api_port_input.text(): api_port = int(self.window().api_port_input.text()) if api_port <= 0 or api_port >= 65536: raise ValueError() self.window().gui_settings.setValue("api_port", api_port) except ValueError: ConfirmationDialog.show_error( self.window(), tr("Invalid value for api port"), tr("Please enter a valid port for the api (between 0 and 65536)" ), ) return seeding_modes = ['forever', 'time', 'never', 'ratio'] selected_mode = 'forever' for seeding_mode in seeding_modes: if getattr(self.window(), "seeding_" + seeding_mode + "_radio").isChecked(): selected_mode = seeding_mode break settings_data['download_defaults']['seeding_mode'] = selected_mode settings_data['download_defaults']['seeding_ratio'] = float( self.window().seeding_ratio_combobox.currentText()) try: settings_data['download_defaults'][ 'seeding_time'] = string_to_seconds( self.window().seeding_time_input.text()) except ValueError: ConfirmationDialog.show_error( self.window(), tr("Invalid seeding time"), tr("You've entered an invalid format for the seeding time (expected HH:MM)" ), ) return settings_data['tunnel_community']['exitnode_enabled'] = self.window( ).allow_exit_node_checkbox.isChecked() settings_data['download_defaults']['number_hops'] = self.window( ).number_hops_slider.value() settings_data['download_defaults']['anonymity_enabled'] = self.window( ).download_settings_anon_checkbox.isChecked() settings_data['download_defaults'][ 'safeseeding_enabled'] = self.window( ).download_settings_anon_seeding_checkbox.isChecked() settings_data['download_defaults'][ 'add_download_to_channel'] = self.window( ).download_settings_add_to_channel_checkbox.isChecked() settings_data['resource_monitor']['enabled'] = self.window( ).checkbox_enable_resource_log.isChecked() settings_data['resource_monitor']['cpu_priority'] = int( self.window().slider_cpu_level.value()) # network statistics settings_data['ipv8']['statistics'] = self.window( ).checkbox_enable_network_statistics.isChecked() self.window().settings_save_button.setEnabled(False) # TODO: do it in RESTful style, on the REST return JSON instead # In case the default save dir has changed, add it to the top of the list of last download locations. # Otherwise, the user could absentmindedly click through the download dialog and start downloading into # the last used download dir, and not into the newly designated default download dir. if self.settings['download_defaults']['saveas'] != settings_data[ 'download_defaults']['saveas']: self.window().update_recent_download_locations( settings_data['download_defaults']['saveas']) self.settings = settings_data TriblerNetworkRequest("settings", self.on_settings_saved, method='POST', raw_data=json.dumps(settings_data)) def on_settings_saved(self, data): if not data: return # Now save the GUI settings self.window().gui_settings.setValue( "family_filter", self.window().family_filter_checkbox.isChecked()) self.window().gui_settings.setValue( "autocommit_enabled", self.window().channel_autocommit_checkbox.isChecked()) self.window().gui_settings.setValue( "ask_download_settings", self.window().always_ask_location_checkbox.isChecked()) self.window().gui_settings.setValue( "use_monochrome_icon", self.window().use_monochrome_icon_checkbox.isChecked()) self.saved_dialog = ConfirmationDialog( TriblerRequestManager.window, tr("Settings saved"), tr("Your settings have been saved."), [(tr("CLOSE"), BUTTON_TYPE_NORMAL)], ) connect(self.saved_dialog.button_clicked, self.on_dialog_cancel_clicked) self.saved_dialog.show() self.window().fetch_settings() def on_dialog_cancel_clicked(self, _): self.window().settings_save_button.setEnabled(True) self.saved_dialog.close_dialog() self.saved_dialog = None
class MarketOrdersPage(QWidget): """ This page displays orders in the decentralized market in Tribler. """ def __init__(self): QWidget.__init__(self) self.initialized = False self.selected_item = None self.dialog = None self.wallets = {} def initialize_orders_page(self, wallets): if not self.initialized: self.window().orders_back_button.setIcon( QIcon(get_image_path('page_back.png'))) self.window().market_orders_list.sortItems(0, Qt.AscendingOrder) self.window( ).market_orders_list.customContextMenuRequested.connect( self.on_right_click_order) self.initialized = True self.wallets = wallets self.load_orders() def load_orders(self): self.window().market_orders_list.clear() TriblerNetworkRequest("market/orders", self.on_received_orders) def on_received_orders(self, orders): if not orders or not self.wallets: return for order in orders["orders"]: if self.has_valid_order_amount(order): asset1_prec = self.wallets[order["assets"]["first"] ["type"]]["precision"] asset2_prec = self.wallets[order["assets"]["second"] ["type"]]["precision"] item = OrderWidgetItem(self.window().market_orders_list, order, asset1_prec, asset2_prec) self.window().market_orders_list.addTopLevelItem(item) def on_right_click_order(self, pos): item_clicked = self.window().market_orders_list.itemAt(pos) if not item_clicked: return self.selected_item = item_clicked if self.selected_item.order[ 'status'] == 'open': # We can only cancel an open order menu = TriblerActionMenu(self) cancel_action = QAction('Cancel order', self) cancel_action.triggered.connect(self.on_cancel_order_clicked) menu.addAction(cancel_action) menu.exec_(self.window().market_orders_list.mapToGlobal(pos)) def on_cancel_order_clicked(self): self.dialog = ConfirmationDialog( self, "Cancel order", "Are you sure you want to cancel the order with id %s?" % self.selected_item.order['order_number'], [('NO', BUTTON_TYPE_NORMAL), ('YES', BUTTON_TYPE_CONFIRM)], ) self.dialog.button_clicked.connect(self.on_confirm_cancel_order) self.dialog.show() def on_confirm_cancel_order(self, action): if action == 1: TriblerNetworkRequest( "market/orders/%s/cancel" % self.selected_item.order['order_number'], self.on_order_cancelled, method='POST', ) self.dialog.close_dialog() self.dialog = None def on_order_cancelled(self, response): if not response: return self.load_orders() def has_valid_order_amount(self, order): return order["assets"]["first"]["amount"] > 0 and order["assets"][ "second"]["amount"] > 0
class CreateTorrentPage(AddBreadcrumbOnShowMixin, QWidget): """ The CreateTorrentPage is the page where users can create torrent files so they can be added to their channel. """ def __init__(self): QWidget.__init__(self) self.channel_identifier = None self.dialog = None self.selected_item_index = -1 self.initialized = False def initialize(self): self.window().create_torrent_name_field.setText('') self.window().create_torrent_description_field.setText('') self.window().create_torrent_files_list.clear() self.window().seed_after_adding_checkbox.setChecked(True) self.window().edit_channel_create_torrent_progress_label.hide() if not self.initialized: self.window().manage_channel_create_torrent_back.setIcon( QIcon(get_image_path('page_back.png'))) connect( self.window().create_torrent_files_list. customContextMenuRequested, self.on_right_click_file_item) connect(self.window().manage_channel_create_torrent_back.clicked, self.on_create_torrent_manage_back_clicked) connect(self.window().create_torrent_choose_files_button.clicked, self.on_choose_files_clicked) connect(self.window().create_torrent_choose_dir_button.clicked, self.on_choose_dir_clicked) connect(self.window().edit_channel_create_torrent_button.clicked, self.on_create_clicked) self.initialized = True def on_create_torrent_manage_back_clicked(self, checked): self.window().edit_channel_details_stacked_widget.setCurrentIndex( PAGE_EDIT_CHANNEL_TORRENTS) def on_choose_files_clicked(self, checked): filenames, _ = QFileDialog.getOpenFileNames(self.window(), "Please select the files", QDir.homePath()) for filename in filenames: self.window().create_torrent_files_list.addItem(filename) def on_choose_dir_clicked(self, checked): chosen_dir = QFileDialog.getExistingDirectory( self.window(), "Please select the directory containing the files", "", QFileDialog.ShowDirsOnly) if len(chosen_dir) == 0: return files = [] for path, _, dir_files in os.walk(chosen_dir): for filename in dir_files: files.append(os.path.join(path, filename)) self.window().create_torrent_files_list.clear() for filename in files: self.window().create_torrent_files_list.addItem(filename) def on_create_clicked(self, checked): if self.window().create_torrent_files_list.count() == 0: self.dialog = ConfirmationDialog( self, "Notice", "You should add at least one file to your torrent.", [('CLOSE', BUTTON_TYPE_NORMAL)]) connect(self.dialog.button_clicked, self.on_dialog_ok_clicked) self.dialog.show() return self.window().edit_channel_create_torrent_button.setEnabled(False) files_list = [] for ind in range(self.window().create_torrent_files_list.count()): file_str = self.window().create_torrent_files_list.item(ind).text() files_list.append(file_str) name = self.window().create_torrent_name_field.text() description = self.window( ).create_torrent_description_field.toPlainText() post_data = { "name": name, "description": description, "files": files_list } url = "createtorrent?download=1" if self.window( ).seed_after_adding_checkbox.isChecked() else "createtorrent" TriblerNetworkRequest(url, self.on_torrent_created, data=post_data, method='POST') # Show creating torrent text self.window().edit_channel_create_torrent_progress_label.show() def on_dialog_ok_clicked(self, _): self.dialog.close_dialog() self.dialog = None def on_torrent_created(self, result): if not result: return self.window().edit_channel_create_torrent_button.setEnabled(True) if 'torrent' in result: self.add_torrent_to_channel(result['torrent']) def add_torrent_to_channel(self, torrent): TriblerNetworkRequest("mychannel/torrents", self.on_torrent_to_channel_added, data={"torrent": torrent}, method='PUT') def on_torrent_to_channel_added(self, result): if not result: return self.window().edit_channel_create_torrent_progress_label.hide() if 'added' in result: self.window().edit_channel_details_stacked_widget.setCurrentIndex( PAGE_EDIT_CHANNEL_TORRENTS) self.window().personal_channel_page.load_my_torrents() def on_remove_entry(self): self.window().create_torrent_files_list.takeItem( self.selected_item_index) def on_right_click_file_item(self, pos): item_clicked = self.window().create_torrent_files_list.itemAt(pos) if not item_clicked: return self.selected_item_index = self.window().create_torrent_files_list.row( item_clicked) menu = TriblerActionMenu(self) remove_action = QAction('Remove file', self) connect(remove_action.triggered, self.on_remove_entry) menu.addAction(remove_action) menu.exec_(self.window().create_torrent_files_list.mapToGlobal(pos))
class DownloadsPage(AddBreadcrumbOnShowMixin, QWidget): """ This class is responsible for managing all items on the downloads page. The downloads page shows all downloads and specific details about a download. """ received_downloads = pyqtSignal(object) def __init__(self): QWidget.__init__(self) self.export_dir = None self.filter = DOWNLOADS_FILTER_ALL self.download_widgets = {} # key: infohash, value: QTreeWidgetItem self.downloads = None self.downloads_timer = QTimer() self.downloads_timeout_timer = QTimer() self.downloads_last_update = 0 self.selected_items = [] self.dialog = None self.loading_message_widget = None self.total_download = 0 self.total_upload = 0 self.rest_request = None def showEvent(self, QShowEvent): """ When the downloads tab is clicked, we want to update the downloads list immediately. """ super().showEvent(QShowEvent) self.stop_loading_downloads() self.schedule_downloads_timer(True) def initialize_downloads_page(self): self.window().downloads_tab.initialize() connect(self.window().downloads_tab.clicked_tab_button, self.on_downloads_tab_button_clicked) connect(self.window().start_download_button.clicked, self.on_start_download_clicked) connect(self.window().stop_download_button.clicked, self.on_stop_download_clicked) connect(self.window().remove_download_button.clicked, self.on_remove_download_clicked) connect(self.window().downloads_list.itemSelectionChanged, self.on_download_item_clicked) connect(self.window().downloads_list.customContextMenuRequested, self.on_right_click_item) self.window().download_details_widget.initialize_details_widget() self.window().download_details_widget.hide() connect(self.window().downloads_filter_input.textChanged, self.on_filter_text_changed) self.window().downloads_list.header().setSortIndicator( 12, Qt.AscendingOrder) self.window().downloads_list.header().resizeSection(12, 146) self.downloads_timeout_timer.setSingleShot(True) self.downloads_timer.setSingleShot(True) connect(self.downloads_timer.timeout, self.load_downloads) connect(self.downloads_timeout_timer.timeout, self.on_downloads_request_timeout) def on_filter_text_changed(self, text): self.window().downloads_list.clearSelection() self.window().download_details_widget.hide() self.update_download_visibility() def start_loading_downloads(self): self.window().downloads_list.setSelectionMode( QAbstractItemView.NoSelection) self.loading_message_widget = QTreeWidgetItem() self.window().downloads_list.addTopLevelItem( self.loading_message_widget) self.window().downloads_list.setItemWidget( self.loading_message_widget, 2, LoadingListItem(self.window().downloads_list)) self.schedule_downloads_timer(now=True) def schedule_downloads_timer(self, now=False): self.downloads_timer.start(0 if now else 1000) self.downloads_timeout_timer.start(16000) def on_downloads_request_timeout(self): if self.rest_request: self.rest_request.cancel_request() self.schedule_downloads_timer() def stop_loading_downloads(self): self.downloads_timer.stop() self.downloads_timeout_timer.stop() def load_downloads(self): url = "downloads?get_pieces=1" if self.window().download_details_widget.currentIndex() == 3: url += "&get_peers=1" elif self.window().download_details_widget.currentIndex() == 1: url += "&get_files=1" isactive = not self.isHidden() if isactive or (time.time() - self.downloads_last_update > 30): # Update if the downloads page is visible or if we haven't updated for longer than 30 seconds self.downloads_last_update = time.time() priority = QNetworkRequest.LowPriority if not isactive else QNetworkRequest.HighPriority if self.rest_request: self.rest_request.cancel_request() self.rest_request = TriblerNetworkRequest( url, self.on_received_downloads, priority=priority) def on_received_downloads(self, downloads): if not downloads or "downloads" not in downloads: return # This might happen when closing Tribler loading_widget_index = self.window( ).downloads_list.indexOfTopLevelItem(self.loading_message_widget) if loading_widget_index > -1: self.window().downloads_list.takeTopLevelItem(loading_widget_index) self.window().downloads_list.setSelectionMode( QAbstractItemView.ExtendedSelection) self.downloads = downloads self.total_download = 0 self.total_upload = 0 download_infohashes = set() items = [] for download in downloads["downloads"]: # Update download progress information for torrents in the Channels GUI. # We skip updating progress information for ChannelTorrents because otherwise it interferes # with channel processing progress updates if not download["channel_download"]: self.window( ).core_manager.events_manager.node_info_updated.emit({ "infohash": download["infohash"], "progress": download["progress"] }) if download["infohash"] in self.download_widgets: item = self.download_widgets[download["infohash"]] else: item = DownloadWidgetItem() self.download_widgets[download["infohash"]] = item items.append(item) item.update_with_download(download) self.total_download += download["speed_down"] self.total_upload += download["speed_up"] download_infohashes.add(download["infohash"]) if (self.window().download_details_widget.current_download is not None and self.window().download_details_widget. current_download["infohash"] == download["infohash"]): self.window( ).download_details_widget.current_download = download self.window().download_details_widget.update_pages() self.window().downloads_list.addTopLevelItems(items) for item in items: self.window().downloads_list.setItemWidget(item, 2, item.bar_container) # Check whether there are download that should be removed for infohash, item in list(self.download_widgets.items()): if infohash not in download_infohashes: index = self.window().downloads_list.indexOfTopLevelItem(item) self.window().downloads_list.takeTopLevelItem(index) del self.download_widgets[infohash] self.window().tray_set_tooltip( f"Down: {format_speed(self.total_download)}, Up: {format_speed(self.total_upload)}" ) self.update_download_visibility() self.schedule_downloads_timer() # Update the top download management button if we have a row selected if len(self.window().downloads_list.selectedItems()) > 0: self.on_download_item_clicked() self.received_downloads.emit(downloads) def update_download_visibility(self): for i in range(self.window().downloads_list.topLevelItemCount()): item = self.window().downloads_list.topLevelItem(i) if not isinstance(item, DownloadWidgetItem): continue filter_match = self.window().downloads_filter_input.text().lower( ) in item.download_info["name"].lower() is_channel = item.download_info["channel_download"] if self.filter == DOWNLOADS_FILTER_CHANNELS: item.setHidden(not is_channel or not filter_match) else: item.setHidden(not item.get_raw_download_status() in DOWNLOADS_FILTER_DEFINITION[self.filter] or not filter_match or is_channel) def on_downloads_tab_button_clicked(self, button_name): self.filter = button_name2filter[button_name] self.window().downloads_list.clearSelection() self.window().download_details_widget.hide() self.update_download_visibility() @staticmethod def start_download_enabled(download_widgets): return any([ download_widget.get_raw_download_status() == DLSTATUS_STOPPED for download_widget in download_widgets ]) @staticmethod def stop_download_enabled(download_widgets): return any([ download_widget.get_raw_download_status() not in [DLSTATUS_STOPPED, DLSTATUS_STOPPED_ON_ERROR] for download_widget in download_widgets ]) @staticmethod def force_recheck_download_enabled(download_widgets): return any([ download_widget.get_raw_download_status() not in [ DLSTATUS_METADATA, DLSTATUS_HASHCHECKING, DLSTATUS_WAITING4HASHCHECK ] for download_widget in download_widgets ]) def on_download_item_clicked(self): selected_count = len(self.window().downloads_list.selectedItems()) if selected_count == 0: self.window().remove_download_button.setEnabled(False) self.window().start_download_button.setEnabled(False) self.window().stop_download_button.setEnabled(False) self.window().download_details_widget.hide() elif selected_count == 1: self.selected_items = self.window().downloads_list.selectedItems() self.window().remove_download_button.setEnabled(True) self.window().start_download_button.setEnabled( DownloadsPage.start_download_enabled(self.selected_items)) self.window().stop_download_button.setEnabled( DownloadsPage.stop_download_enabled(self.selected_items)) self.window().download_details_widget.update_with_download( self.selected_items[0].download_info) self.window().download_details_widget.show() else: self.selected_items = self.window().downloads_list.selectedItems() self.window().remove_download_button.setEnabled(True) self.window().start_download_button.setEnabled( DownloadsPage.start_download_enabled(self.selected_items)) self.window().stop_download_button.setEnabled( DownloadsPage.stop_download_enabled(self.selected_items)) self.window().download_details_widget.hide() def on_start_download_clicked(self, checked): for selected_item in self.selected_items: infohash = selected_item.download_info["infohash"] TriblerNetworkRequest(f"downloads/{infohash}", self.on_download_resumed, method='PATCH', data={"state": "resume"}) def on_download_resumed(self, json_result): if json_result and 'modified' in json_result: for selected_item in self.selected_items: if selected_item.download_info["infohash"] == json_result[ "infohash"]: selected_item.download_info[ 'status'] = "DLSTATUS_DOWNLOADING" selected_item.update_item() self.on_download_item_clicked() def on_stop_download_clicked(self, checked): for selected_item in self.selected_items: infohash = selected_item.download_info["infohash"] TriblerNetworkRequest(f"downloads/{infohash}", self.on_download_stopped, method='PATCH', data={"state": "stop"}) def on_download_stopped(self, json_result): if json_result and "modified" in json_result: for selected_item in self.selected_items: if selected_item.download_info["infohash"] == json_result[ "infohash"]: selected_item.download_info['status'] = "DLSTATUS_STOPPED" selected_item.update_item() self.on_download_item_clicked() def on_remove_download_clicked(self, checked): self.dialog = ConfirmationDialog( self, tr("Remove download"), tr("Are you sure you want to remove this download?"), [ (tr("remove download"), BUTTON_TYPE_NORMAL), (tr("remove download + data"), BUTTON_TYPE_NORMAL), (tr("cancel"), BUTTON_TYPE_CONFIRM), ], ) connect(self.dialog.button_clicked, self.on_remove_download_dialog) self.dialog.show() def on_remove_download_dialog(self, action): if action != 2: for selected_item in self.selected_items: infohash = selected_item.download_info["infohash"] TriblerNetworkRequest( f"downloads/{infohash}", self.on_download_removed, method='DELETE', data={"remove_data": bool(action)}, ) if self.dialog: self.dialog.close_dialog() self.dialog = None def on_download_removed(self, json_result): if json_result and "removed" in json_result: self.load_downloads() self.window().download_details_widget.hide() def on_force_recheck_download(self, checked): for selected_item in self.selected_items: infohash = selected_item.download_info["infohash"] TriblerNetworkRequest(f"downloads/{infohash}", self.on_forced_recheck, method='PATCH', data={"state": "recheck"}) def on_forced_recheck(self, result): if result and "modified" in result: for selected_item in self.selected_items: if selected_item.download_info["infohash"] == result[ "infohash"]: selected_item.download_info[ 'status'] = "DLSTATUS_HASHCHECKING" selected_item.update_item() self.on_download_item_clicked() def on_change_anonymity(self, result): pass def change_anonymity(self, hops): for selected_item in self.selected_items: infohash = selected_item.download_info["infohash"] TriblerNetworkRequest(f"downloads/{infohash}", self.on_change_anonymity, method='PATCH', data={"anon_hops": hops}) def on_explore_files(self, checked): # ACHTUNG! To whomever might stumble upon here intending to debug the case # when this does not work on Linux: know, my friend, that for some mysterious reason # (probably related to Snap disk access rights peculiarities), this DOES NOT work # when you run Tribler from PyCharm. However, it works perfectly fine when you # run Tribler directly from system console, etc. So, don't spend your time on debugging this, # like I did. for selected_item in self.selected_items: path = os.path.normpath(selected_item.download_info["destination"]) QDesktopServices.openUrl(QUrl.fromLocalFile(path)) def on_move_files(self, checked): if len(self.selected_items) != 1: return dest_dir = QFileDialog.getExistingDirectory( self, tr("Please select the destination directory"), self.selected_items[0].download_info["destination"], QFileDialog.ShowDirsOnly, ) if not dest_dir: return _infohash = self.selected_items[0].download_info["infohash"] _name = self.selected_items[0].download_info["name"] data = {"state": "move_storage", "dest_dir": dest_dir} TriblerNetworkRequest( f"downloads/{_infohash}", lambda res: self.on_files_moved(res, _name, dest_dir), data=data, method='PATCH', ) def on_files_moved(self, response, name, dest_dir): if "modified" in response and response["modified"]: self.window().tray_show_message(name, f"Moved to {dest_dir}") def on_export_download(self, checked): self.export_dir = QFileDialog.getExistingDirectory( self, tr("Please select the destination directory"), "", QFileDialog.ShowDirsOnly) selected_item = self.selected_items[:1] if len(self.export_dir) > 0 and selected_item: # Show confirmation dialog where we specify the name of the file torrent_name = selected_item[0].download_info['name'] self.dialog = ConfirmationDialog( self, tr("Export torrent file"), tr("Please enter the name of the torrent file:"), [(tr("SAVE"), BUTTON_TYPE_NORMAL), (tr("CANCEL"), BUTTON_TYPE_CONFIRM)], show_input=True, ) self.dialog.dialog_widget.dialog_input.setPlaceholderText( tr("Torrent file name")) self.dialog.dialog_widget.dialog_input.setText( f"{torrent_name}.torrent") self.dialog.dialog_widget.dialog_input.setFocus() connect(self.dialog.button_clicked, self.on_export_download_dialog_done) self.dialog.show() def on_export_download_dialog_done(self, action): selected_item = self.selected_items[:1] if action == 0 and selected_item: filename = self.dialog.dialog_widget.dialog_input.text() TriblerFileDownloadRequest( f"downloads/{selected_item[0].download_info['infohash']}/torrent", lambda data: self.on_export_download_request_done( filename, data), ) self.dialog.close_dialog() self.dialog = None def on_export_download_request_done(self, filename, data): dest_path = os.path.join(self.export_dir, filename) try: torrent_file = open(dest_path, "wb") torrent_file.write(data) torrent_file.close() except OSError as exc: ConfirmationDialog.show_error( self.window(), tr("Error when exporting file"), tr("An error occurred when exporting the torrent file: %s") % str(exc), ) else: self.window().tray_show_message( tr("Torrent file exported"), tr("Torrent file exported to %s") % str(dest_path)) def on_add_to_channel(self, checked): def on_add_button_pressed(channel_id): for selected_item in self.selected_items: infohash = selected_item.download_info["infohash"] name = selected_item.download_info["name"] TriblerNetworkRequest( f"channels/mychannel/{channel_id}/torrents", lambda _: self.window().tray_show_message( tr("Channel update"), tr("Torrent(s) added to your channel")), method='PUT', data={"uri": compose_magnetlink(infohash, name=name)}, ) self.window().add_to_channel_dialog.show_dialog( on_add_button_pressed, confirm_button_text=tr("Add torrent(s)")) def on_right_click_item(self, pos): item_clicked = self.window().downloads_list.itemAt(pos) if not item_clicked or self.selected_items is None: return if item_clicked not in self.selected_items: self.selected_items.append(item_clicked) menu = TriblerActionMenu(self) start_action = QAction(tr("Start"), self) stop_action = QAction(tr("Stop"), self) remove_download_action = QAction(tr("Remove download"), self) add_to_channel_action = QAction(tr("Add to my channel"), self) force_recheck_action = QAction(tr("Force recheck"), self) export_download_action = QAction(tr("Export .torrent file"), self) explore_files_action = QAction(tr("Explore files"), self) move_files_action = QAction(tr("Move file storage"), self) no_anon_action = QAction(tr("No anonymity"), self) one_hop_anon_action = QAction(tr("One hop"), self) two_hop_anon_action = QAction(tr("Two hops"), self) three_hop_anon_action = QAction(tr("Three hops"), self) connect(start_action.triggered, self.on_start_download_clicked) start_action.setEnabled( DownloadsPage.start_download_enabled(self.selected_items)) connect(stop_action.triggered, self.on_stop_download_clicked) stop_action.setEnabled( DownloadsPage.stop_download_enabled(self.selected_items)) connect(add_to_channel_action.triggered, self.on_add_to_channel) connect(remove_download_action.triggered, self.on_remove_download_clicked) connect(force_recheck_action.triggered, self.on_force_recheck_download) force_recheck_action.setEnabled( DownloadsPage.force_recheck_download_enabled(self.selected_items)) connect(export_download_action.triggered, self.on_export_download) connect(explore_files_action.triggered, self.on_explore_files) connect(move_files_action.triggered, self.on_move_files) connect(no_anon_action.triggered, lambda _: self.change_anonymity(0)) connect(one_hop_anon_action.triggered, lambda _: self.change_anonymity(1)) connect(two_hop_anon_action.triggered, lambda _: self.change_anonymity(2)) connect(three_hop_anon_action.triggered, lambda _: self.change_anonymity(3)) menu.addAction(start_action) menu.addAction(stop_action) menu.addSeparator() menu.addAction(add_to_channel_action) menu.addSeparator() menu.addAction(remove_download_action) menu.addSeparator() menu.addAction(force_recheck_action) menu.addSeparator() exclude_states = [ DLSTATUS_METADATA, DLSTATUS_CIRCUITS, DLSTATUS_EXIT_NODES, DLSTATUS_HASHCHECKING, DLSTATUS_WAITING4HASHCHECK, ] if len(self.selected_items) == 1 and self.selected_items[ 0].get_raw_download_status() not in exclude_states: menu.addAction(export_download_action) menu.addAction(explore_files_action) if len(self.selected_items) == 1: menu.addAction(move_files_action) menu.addSeparator() menu_anon_level = menu.addMenu(tr("Change Anonymity ")) menu_anon_level.addAction(no_anon_action) menu_anon_level.addAction(one_hop_anon_action) menu_anon_level.addAction(two_hop_anon_action) menu_anon_level.addAction(three_hop_anon_action) menu.exec_(self.window().downloads_list.mapToGlobal(pos))
def perform_start_download_request( self, uri, anon_download, safe_seeding, destination, selected_files, total_files=0, add_to_channel=False, callback=None, ): # Check if destination directory is writable is_writable, error = is_dir_writable(destination) if not is_writable: gui_error_message = ( "Insufficient write permissions to <i>%s</i> directory. Please add proper " "write permissions on the directory and add the torrent again. %s" % (destination, error)) ConfirmationDialog.show_message(self.window(), f"Download error <i>{uri}</i>", gui_error_message, "OK") return selected_files_list = [] if len(selected_files) != total_files: # Not all files included selected_files_list = [filename for filename in selected_files] anon_hops = int(self.tribler_settings['download_defaults'] ['number_hops']) if anon_download else 0 safe_seeding = 1 if safe_seeding else 0 post_data = { "uri": uri, "anon_hops": anon_hops, "safe_seeding": safe_seeding, "destination": destination, "selected_files": selected_files_list, } TriblerNetworkRequest("downloads", callback if callback else self.on_download_added, method='PUT', data=post_data) self.update_recent_download_locations(destination) if add_to_channel: def on_add_button_pressed(channel_id): post_data = {} if uri.startswith("file:"): with open(uri[5:], "rb") as torrent_file: post_data['torrent'] = b64encode( torrent_file.read()).decode('utf8') elif uri.startswith("magnet:"): post_data['uri'] = uri if post_data: TriblerNetworkRequest( f"channels/mychannel/{channel_id}/torrents", lambda _: self.tray_show_message( "Channel update", "Torrent(s) added to your channel"), method='PUT', data=post_data, ) self.window().add_to_channel_dialog.show_dialog( on_add_button_pressed, confirm_button_text="Add torrent")