diff --git a/src/core/manager.py b/src/core/manager.py index 32093c6..5e883de 100644 --- a/src/core/manager.py +++ b/src/core/manager.py @@ -159,7 +159,7 @@ class DownloadManager: if self.thread_pool: self.thread_pool.shutdown(wait=True) self.is_running = False - self._log("š All processing tasks have completed.") + self._log("š All processing tasks have completed or been cancelled.") self.progress_queue.put({ 'type': 'finished', 'payload': (self.total_downloads, self.total_skips, self.cancellation_event.is_set(), self.all_kept_original_filenames) diff --git a/src/core/workers.py b/src/core/workers.py index 1434d7f..6e8fba4 100644 --- a/src/core/workers.py +++ b/src/core/workers.py @@ -887,17 +887,6 @@ class PostProcessorWorker: result_tuple = (0, num_potential_files_in_post, [], [], [], None, None) return result_tuple - if self.skip_words_list and (self.skip_words_scope == SKIP_SCOPE_POSTS or self.skip_words_scope == SKIP_SCOPE_BOTH): - if self._check_pause(f"Skip words (post title) for post {post_id}"): - result_tuple = (0, num_potential_files_in_post, [], [], [], None, None) - return result_tuple - post_title_lower = post_title.lower() - for skip_word in self.skip_words_list: - if skip_word.lower() in post_title_lower: - self.logger(f" -> Skip Post (Keyword in Title '{skip_word}'): '{post_title[:50]}...'. Scope: {self.skip_words_scope}") - result_tuple = (0, num_potential_files_in_post, [], [], [], None, None) - return result_tuple - if not self.extract_links_only and self.manga_mode_active and current_character_filters and (self.char_filter_scope == CHAR_SCOPE_TITLE or self.char_filter_scope == CHAR_SCOPE_BOTH) and not post_is_candidate_by_title_char_match: self.logger(f" -> Skip Post (Manga Mode with Title/Both Scope - No Title Char Match): Title '{post_title[:50]}' doesn't match filters.") self._emit_signal('missed_character_post', post_title, "Manga Mode: No title match for character filter (Title/Both scope)") @@ -908,6 +897,7 @@ class PostProcessorWorker: self.logger(f"ā ļø Corrupt attachment data for post {post_id} (expected list, got {type(post_attachments)}). Skipping attachments.") post_attachments = [] + # CORRECTED LOGIC: Determine folder path BEFORE skip checks base_folder_names_for_post_content = [] determined_post_save_path_for_history = self.override_output_dir if self.override_output_dir else self.download_root if not self.extract_links_only and self.use_subfolders: @@ -1056,6 +1046,28 @@ class PostProcessorWorker: break determined_post_save_path_for_history = os.path.join(base_path_for_post_subfolder, final_post_subfolder_name) + if self.skip_words_list and (self.skip_words_scope == SKIP_SCOPE_POSTS or self.skip_words_scope == SKIP_SCOPE_BOTH): + if self._check_pause(f"Skip words (post title) for post {post_id}"): + result_tuple = (0, num_potential_files_in_post, [], [], [], None, None) + return result_tuple + post_title_lower = post_title.lower() + for skip_word in self.skip_words_list: + if skip_word.lower() in post_title_lower: + self.logger(f" -> Skip Post (Keyword in Title '{skip_word}'): '{post_title[:50]}...'. Scope: {self.skip_words_scope}") + # Create a history object for the skipped post to record its ID + history_data_for_skipped_post = { + 'post_id': post_id, + 'service': self.service, + 'user_id': self.user_id, + 'post_title': post_title, + 'top_file_name': "N/A (Post Skipped)", + 'num_files': num_potential_files_in_post, + 'upload_date_str': post_data.get('published') or post_data.get('added') or "Unknown", + 'download_location': determined_post_save_path_for_history + } + result_tuple = (0, num_potential_files_in_post, [], [], [], history_data_for_skipped_post, None) + return result_tuple + if self.filter_mode == 'text_only' and not self.extract_links_only: self.logger(f" Mode: Text Only (Scope: {self.text_only_scope})") post_title_lower = post_title.lower() diff --git a/src/ui/dialogs/EmptyPopupDialog.py b/src/ui/dialogs/EmptyPopupDialog.py index 422d1b6..5978883 100644 --- a/src/ui/dialogs/EmptyPopupDialog.py +++ b/src/ui/dialogs/EmptyPopupDialog.py @@ -969,6 +969,9 @@ class EmptyPopupDialog (QDialog ): self .parent_app .link_input .setPlaceholderText ( self .parent_app ._tr ("items_in_queue_placeholder","{count} items in queue from popup.").format (count =total_in_queue ) ) + + self.selected_creators_for_queue.clear() + self .accept () else : QMessageBox .information (self ,self ._tr ("no_selection_title","No Selection"), diff --git a/src/ui/main_window.py b/src/ui/main_window.py index 31fd61c..4938fec 100644 --- a/src/ui/main_window.py +++ b/src/ui/main_window.py @@ -233,6 +233,7 @@ class DownloaderApp (QWidget ): self.downloaded_hash_counts = defaultdict(int) self.downloaded_hash_counts_lock = threading.Lock() self.session_temp_files = [] + self.single_pdf_mode = False self.save_creator_json_enabled_this_session = True print(f"ā¹ļø Known.txt will be loaded/saved at: {self.config_file}") @@ -1429,15 +1430,21 @@ class DownloaderApp (QWidget ): def _check_if_all_work_is_done(self): """ - Checks if the fetcher thread is done AND if all submitted tasks have been processed. - If so, finalizes the download. + Checks if the fetcher thread is done AND if all submitted tasks have been processed OR if a cancellation was requested. + If so, finalizes the download. This is the central point for completion logic. """ fetcher_is_done = not self.is_fetcher_thread_running - all_workers_are_done = (self.total_posts_to_process > 0 and self.processed_posts_count >= self.total_posts_to_process) + all_workers_are_done = (self.processed_posts_count >= self.total_posts_to_process) + is_cancelled = self.cancellation_event.is_set() - if fetcher_is_done and all_workers_are_done: - self.log_signal.emit("š All fetcher and worker tasks complete.") - self.finished_signal.emit(self.download_counter, self.skip_counter, self.cancellation_event.is_set(), self.all_kept_original_filenames) + if fetcher_is_done and (all_workers_are_done or is_cancelled): + if not self.is_finishing: + if is_cancelled: + self.log_signal.emit("š Fetcher cancelled. Finalizing...") + else: + self.log_signal.emit("š All fetcher and worker tasks complete. Finalizing...") + + self.finished_signal.emit(self.download_counter, self.skip_counter, is_cancelled, self.all_kept_original_filenames) def _sync_queue_with_link_input (self ,current_text ): """ @@ -4160,49 +4167,34 @@ class DownloaderApp (QWidget ): self ._update_log_display_mode_button_text () self ._filter_links_log () - def cancel_download_button_action (self ): - self.is_finishing = True - if not self .cancel_btn .isEnabled ()and not self .cancellation_event .is_set ():self .log_signal .emit ("ā¹ļø No active download to cancel or already cancelling.");return - self .log_signal .emit ("ā ļø Requesting cancellation of download process (soft reset)...") - self._cleanup_temp_files() - self._clear_session_file() # Clear session file on explicit cancel - if self .external_link_download_thread and self .external_link_download_thread .isRunning (): - self .log_signal .emit (" Cancelling active External Link download thread...") - self .external_link_download_thread .cancel () + def cancel_download_button_action(self): + """ + Signals all active download processes to cancel but DOES NOT reset the UI. + The UI reset is now handled by the 'download_finished' method. + """ + if self.cancellation_event.is_set(): + self.log_signal.emit("ā¹ļø Cancellation is already in progress.") + return - current_url =self .link_input .text () - current_dir =self .dir_input .text () + self.log_signal.emit("ā ļø Requesting cancellation of download process...") + self.cancellation_event.set() - self .cancellation_event .set () - self .is_fetcher_thread_running =False - if self .download_thread and self .download_thread .isRunning ():self .download_thread .requestInterruption ();self .log_signal .emit (" Signaled single download thread to interrupt.") - if self .thread_pool : - self .log_signal .emit (" Initiating non-blocking shutdown and cancellation of worker pool tasks...") - self .thread_pool .shutdown (wait =False ,cancel_futures =True ) - self .thread_pool =None - self .active_futures =[] + # Update UI to "Cancelling" state + self.pause_btn.setEnabled(False) + self.cancel_btn.setEnabled(False) + self.progress_label.setText(self._tr("status_cancelling", "Cancelling... Please wait.")) - self .external_link_queue .clear ();self ._is_processing_external_link_queue =False ;self ._current_link_post_title =None + # Signal all active components to stop + if self.download_thread and self.download_thread.isRunning(): + self.download_thread.requestInterruption() + self.log_signal.emit(" Signaled single download thread to interrupt.") - self ._perform_soft_ui_reset (preserve_url =current_url ,preserve_dir =current_dir ) - - self .progress_label .setText (f"{self ._tr ('status_cancelled_by_user','Cancelled by user')}. {self ._tr ('ready_for_new_task_text','Ready for new task.')}") - self .file_progress_label .setText ("") - if self .pause_event :self .pause_event .clear () - self .log_signal .emit ("ā¹ļø UI reset. Ready for new operation. Background tasks are being terminated.") - self .is_paused =False - if hasattr (self ,'retryable_failed_files_info')and self .retryable_failed_files_info : - self .log_signal .emit (f" Discarding {len (self .retryable_failed_files_info )} pending retryable file(s) due to cancellation.") - self .cancellation_message_logged_this_session =False - self .retryable_failed_files_info .clear () - self .favorite_download_queue .clear () - self .permanently_failed_files_for_dialog .clear () - self .is_processing_favorites_queue =False - self .favorite_download_scope =FAVORITE_SCOPE_SELECTED_LOCATION - self ._update_favorite_scope_button_text () - if hasattr (self ,'link_input'): - self .last_link_input_text_for_queue_sync =self .link_input .text () - self .cancellation_message_logged_this_session =False + if self.thread_pool: + self.log_signal.emit(" Signaling worker pool to cancel futures...") + + if self.external_link_download_thread and self.external_link_download_thread.isRunning(): + self.log_signal.emit(" Cancelling active External Link download thread...") + self.external_link_download_thread.cancel() def _get_domain_for_service (self ,service_name :str )->str : """Determines the base domain for a given service.""" @@ -4220,119 +4212,129 @@ class DownloaderApp (QWidget ): return self.is_finishing = True - self.log_signal.emit("š Download of current item complete.") + try: + if cancelled_by_user: + self.log_signal.emit("ā Cancellation complete. Resetting UI.") + current_url = self.link_input.text() + current_dir = self.dir_input.text() + self._perform_soft_ui_reset(preserve_url=current_url, preserve_dir=current_dir) + self.progress_label.setText(f"{self._tr('status_cancelled_by_user', 'Cancelled by user')}. {self._tr('ready_for_new_task_text', 'Ready for new task.')}") + self.file_progress_label.setText("") + if self.pause_event: self.pause_event.clear() + self.is_paused = False + return # Exit after handling cancellation - if self.is_processing_favorites_queue and self.favorite_download_queue: - self.log_signal.emit("ā Item finished. Processing next in queue...") - self._process_next_favorite_download() - return + self.log_signal.emit("š Download of current item complete.") - if self.is_processing_favorites_queue: - self.is_processing_favorites_queue = False - self.log_signal.emit("ā All items from the download queue have been processed.") + if self.is_processing_favorites_queue and self.favorite_download_queue: + self.log_signal.emit("ā Item finished. Processing next in queue...") + self.is_finishing = False # Allow the next item in queue to start + self._process_next_favorite_download() + return - if not cancelled_by_user and not self.retryable_failed_files_info: - self._clear_session_file() - self.interrupted_session_data = None - self.is_restore_pending = False + if self.is_processing_favorites_queue: + self.is_processing_favorites_queue = False + self.log_signal.emit("ā All items from the download queue have been processed.") - self._finalize_download_history() - status_message = self._tr("status_cancelled_by_user", "Cancelled by user") if cancelled_by_user else self._tr("status_completed", "Completed") - if cancelled_by_user and self.retryable_failed_files_info: - self.log_signal.emit(f" Download cancelled, discarding {len(self.retryable_failed_files_info)} file(s) that were pending retry.") - self.retryable_failed_files_info.clear() + if not cancelled_by_user and not self.retryable_failed_files_info: + self._clear_session_file() + self.interrupted_session_data = None + self.is_restore_pending = False - summary_log = "=" * 40 - summary_log += f"\nš Download {status_message}!\n Summary: Downloaded Files={total_downloaded}, Skipped Files={total_skipped}\n" - summary_log += "=" * 40 - self.log_signal.emit(summary_log) - self.log_signal.emit("") - - if self.thread_pool: - self.log_signal.emit(" Shutting down worker thread pool...") - self.thread_pool.shutdown(wait=False) - self.thread_pool = None - self.log_signal.emit(" Thread pool shut down.") + self._finalize_download_history() + status_message = self._tr("status_completed", "Completed") - if self.single_pdf_setting and self.session_temp_files and not cancelled_by_user: - try: - self._trigger_single_pdf_creation() - finally: + summary_log = "=" * 40 + summary_log += f"\nš Download {status_message}!\n Summary: Downloaded Files={total_downloaded}, Skipped Files={total_skipped}\n" + summary_log += "=" * 40 + self.log_signal.emit(summary_log) + self.log_signal.emit("") + + if self.thread_pool: + self.thread_pool.shutdown(wait=False) + self.thread_pool = None + + if self.single_pdf_setting and self.session_temp_files: + try: + self._trigger_single_pdf_creation() + finally: + self._cleanup_temp_files() + else: self._cleanup_temp_files() self.single_pdf_setting = False - else: - self._cleanup_temp_files() - self.single_pdf_setting = False - if kept_original_names_list is None: - kept_original_names_list = list(self.all_kept_original_filenames) if hasattr(self, 'all_kept_original_filenames') else [] - if kept_original_names_list is None: - kept_original_names_list = [] + if kept_original_names_list is None: + kept_original_names_list = list(self.all_kept_original_filenames) if hasattr(self, 'all_kept_original_filenames') else [] + if kept_original_names_list is None: + kept_original_names_list = [] - if kept_original_names_list: - intro_msg = ( - HTML_PREFIX + - "
ā¹ļø The following files from multi-file manga posts " - "(after the first file) kept their original names:
" + if kept_original_names_list: + intro_msg = ( + HTML_PREFIX + + "ā¹ļø The following files from multi-file manga posts " + "(after the first file) kept their original names:
" + ) + self.log_signal.emit(intro_msg) + html_list_items = "