From a2dc28f4d40395e17abc3d58a431cd8f0581ca78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deniz=20=C5=9Eafak?= Date: Wed, 17 Sep 2025 19:10:47 +0300 Subject: [PATCH] Fixed save options not working correctly, better indicators --- CHANGELOG.md | 4 +- abogen/conversion.py | 58 ++++++++------ abogen/gui.py | 79 ++++++++++++++++-- abogen/queue_manager_gui.py | 155 +++++++++++++++++++++++++++++------- abogen/queued_item.py | 1 + 5 files changed, 236 insertions(+), 61 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5994e43..8e4c6a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,9 @@ # 1.1.8 (pre-release) - Added `.md` (Markdown) file extension support by @brianxiadong in PR #75 - Added new option `Configure silence between chapters` that lets you configure the silence between chapters, mentioned by @lfperez1982 in #79 -- Improved the markdown logic to better handle various markdown structures and edge cases. +- Better indicators while displaying and managing the input and processing files. +- Improved the markdown logic to better handle various markdown structures and cases. +- Fixed save options not working correctly in queue mode, mentioned by @jborza in #78 - Fixed `No Qt platform plugin could be initialized` error, mentioned by @sunrainxyz in #59 - Potentially fixed subtitle generation stucks at 9:59:59, mentioned by @bolaykim in #73 - Improvements in code and documentation. diff --git a/abogen/conversion.py b/abogen/conversion.py index eeb5deb..d980b2a 100644 --- a/abogen/conversion.py +++ b/abogen/conversion.py @@ -148,6 +148,7 @@ class ConversionThread(QThread): total_char_count, use_gpu=True, from_queue=False, + save_base_path=None, ): # Add use_gpu parameter super().__init__() self._chapter_options_event = threading.Event() @@ -169,6 +170,7 @@ class ConversionThread(QThread): self.total_char_count = total_char_count # Use passed total character count self.processed_char_count = 0 # Initialize processed character count self.display_path = None # Add variable for display path + self.save_base_path = save_base_path # Store the save base path self.is_direct_text = ( False # Flag to indicate if input is from textbox rather than file ) @@ -238,15 +240,23 @@ class ConversionThread(QThread): # Show configuration self.log_updated.emit("Configuration:") + # Determine input file and processing file + if getattr(self, "from_queue", False): + input_file = self.save_base_path or self.file_name + processing_file = self.file_name + else: + input_file = self.display_path if self.display_path else self.file_name + processing_file = self.file_name + + self.log_updated.emit(f"- Input File: {input_file}") + if input_file != processing_file: + self.log_updated.emit(f"- Processing File: {processing_file}") + # Use file_name for logs if from_queue, otherwise use display_path if available if getattr(self, "from_queue", False): - display_file = self.file_name + base_path = self.save_base_path or self.file_name # Use save_base_path if available else: - display_file = ( - self.display_path if self.display_path else self.file_name - ) - - self.log_updated.emit(f"- Input File: {display_file}") + base_path = self.display_path if self.display_path else self.file_name # Use file size string passed from GUI if hasattr(self, "file_size_str"): @@ -402,7 +412,7 @@ class ConversionThread(QThread): # Use file_name for logs if from_queue, otherwise use display_path if available if getattr(self, "from_queue", False): - base_path = self.file_name + base_path = self.save_base_path or self.file_name # Use save_base_path if available else: base_path = self.display_path if self.display_path else self.file_name @@ -607,6 +617,10 @@ class ConversionThread(QThread): # Instead of processing the whole text, process by chapter for chapter_idx, (chapter_name, chapter_text) in enumerate(chapters, 1): chapter_out_path = None + chapter_out_file = None + chapter_ffmpeg_proc = None + chapter_subtitle_file = None + chapter_subtitle_path = None if total_chapters > 1: self.log_updated.emit( ( @@ -720,27 +734,21 @@ class ConversionThread(QThread): is_narrow = subtitle_format in ( "ass_narrow", "ass_centered_narrow", - ) - chapter_subtitle_margin = "90" if is_narrow else "" - chapter_subtitle_alignment_tag = ( - f"{{\\an5}}" if is_centered else "" - ) - else: - chapter_subtitle_file = open( - chapter_subtitle_path, - "w", - encoding="utf-8", - errors="replace", - ) + ) + chapter_subtitle_margin = "90" if is_narrow else "" + chapter_subtitle_alignment_tag = ( + f"{{\\an5}}" if is_centered else "" + ) else: - chapter_subtitle_path = None - chapter_subtitle_file = None + chapter_subtitle_file = open( + chapter_subtitle_path, + "w", + encoding="utf-8", + errors="replace", + ) else: - chapter_out_file = None - chapter_out_path = None - chapter_ffmpeg_proc = None - chapter_subtitle_file = None chapter_subtitle_path = None + chapter_subtitle_file = None for result in tts( chapter_text, voice=loaded_voice, diff --git a/abogen/gui.py b/abogen/gui.py index 607f98d..ce348c7 100644 --- a/abogen/gui.py +++ b/abogen/gui.py @@ -486,15 +486,63 @@ class InputBox(QLabel): # win.selected_file holds the path to the text that is converted. file_to_check = win.selected_file + # If this is a converted document (epub/pdf/markdown) that was written to the + # user's cache directory, show a menu letting the user jump to either the + # processed (cached .txt) file or the original input file (epub/pdf/md). + try: + cache_dir = get_user_cache_path() + except Exception: + cache_dir = None + + is_cached_doc = False if ( file_to_check and os.path.exists(file_to_check) and os.path.isfile(file_to_check) + and cache_dir ): - folder_path = os.path.dirname(file_to_check) - QDesktopServices.openUrl(QUrl.fromLocalFile(folder_path)) + # Consider it cached when the file is under the cache directory and is a .txt + if file_to_check.endswith('.txt') and os.path.commonpath([os.path.abspath(file_to_check), os.path.abspath(cache_dir)]) == os.path.abspath(cache_dir): + # Only treat as document-cache when original type was a document + if getattr(win, 'selected_file_type', None) in ['epub', 'pdf', 'md', 'markdown']: + is_cached_doc = True + + if is_cached_doc: + menu = QMenu(self) + act_processed = QAction("Go to processed file", self) + + def open_processed(): + folder_path = os.path.dirname(file_to_check) + QDesktopServices.openUrl(QUrl.fromLocalFile(folder_path)) + + act_processed.triggered.connect(open_processed) + menu.addAction(act_processed) + + act_input = QAction("Go to input file", self) + # Prefer displayed_file_path (original input path) then selected_book_path + input_path = getattr(win, 'displayed_file_path', None) or getattr(win, 'selected_book_path', None) + if input_path and os.path.exists(input_path): + def open_input(): + folder_path = os.path.dirname(input_path) + QDesktopServices.openUrl(QUrl.fromLocalFile(folder_path)) + + act_input.triggered.connect(open_input) + else: + act_input.setEnabled(False) + + menu.addAction(act_input) + # Show the menu anchored to the button + menu.exec_(self.go_to_folder_btn.mapToGlobal(QPoint(0, self.go_to_folder_btn.height()))) else: - QMessageBox.warning(win, "Error", "Converted file not found.") + if ( + file_to_check + and os.path.exists(file_to_check) + and os.path.isfile(file_to_check) + ): + folder_path = os.path.dirname(file_to_check) + QDesktopServices.openUrl(QUrl.fromLocalFile(folder_path)) + else: + QMessageBox.warning(win, "Error", "Converted file not found.") class TextboxDialog(QDialog): @@ -1644,13 +1692,16 @@ class abogen(QWidget): # For epub/pdf, always use the converted txt file (selected_file) if self.selected_file_type in ["epub", "pdf", "md", "markdown"]: file_to_queue = self.selected_file + # Use the original file path for save location + save_base_path = self.displayed_file_path if self.displayed_file_path else file_to_queue else: file_to_queue = ( self.displayed_file_path if self.displayed_file_path else self.selected_file ) - + save_base_path = file_to_queue # For non-EPUB, it's the same + if not file_to_queue: self.input_box.set_error("Please add a file.") return @@ -1669,6 +1720,7 @@ class abogen(QWidget): output_format=self.selected_format, total_char_count=self.char_count, replace_single_newlines=self.replace_single_newlines, + save_base_path=save_base_path, ) # Prevent adding duplicate items to the queue @@ -1684,6 +1736,8 @@ class abogen(QWidget): and queued_item.output_format == item_queue.output_format and getattr(queued_item, "replace_single_newlines", False) == item_queue.replace_single_newlines + and getattr(queued_item, "save_base_path", None) + == item_queue.save_base_path ): QMessageBox.warning( self, "Duplicate Item", "This item is already in the queue." @@ -1743,6 +1797,8 @@ class abogen(QWidget): self.replace_single_newlines = getattr( queued_item, "replace_single_newlines", False ) + # Restore the original file path for save location + self.displayed_file_path = queued_item.save_base_path or queued_item.file_name self.start_conversion(from_queue=True) else: # Queue finished, reset index @@ -1786,6 +1842,18 @@ class abogen(QWidget): if not self.selected_file: self.input_box.set_error("Please add a file.") return + + # Ensure we honor the currently selected save option when not running from queue + if not from_queue: + current_option = self.save_combo.currentText() + self.save_option = current_option + self.config["save_option"] = current_option + # If user is not choosing a specific folder, clear any residual folder + if current_option != "Choose output folder": + self.selected_output_folder = None + self.config["selected_output_folder"] = None + save_config(self.config) + prevent_sleep_start() self.is_converting = True self.convert_input_box_to_log() @@ -1858,6 +1926,7 @@ class abogen(QWidget): total_char_count=self.char_count, use_gpu=self.gpu_ok, from_queue=from_queue, + save_base_path=self.displayed_file_path, # Pass the save base path (original file for EPUB) ) # Use gpu_ok status # Pass the displayed file path to the log_updated signal handler in ConversionThread self.conversion_thread.display_path = display_path @@ -2981,7 +3050,7 @@ class abogen(QWidget): def open_cache_directory(self): """Open the cache directory used by the program.""" try: - # Get the cache directory path + # Get the abogen cache directory cache_dir = get_user_cache_path() # Create the directory if it doesn't exist diff --git a/abogen/queue_manager_gui.py b/abogen/queue_manager_gui.py index d7dea99..d8514b2 100644 --- a/abogen/queue_manager_gui.py +++ b/abogen/queue_manager_gui.py @@ -217,20 +217,33 @@ class QueueManager(QDialog): self.empty_overlay.hide() icon_provider = QFileIconProvider() for item in self.queue: + # Determine display file path (prefer save_base_path for original file) + display_file_path = getattr(item, "save_base_path", None) or item.file_name + processing_file_path = item.file_name + # Only show the file name, not the full path - file_name = item.file_name - display_name = file_name + display_name = display_file_path import os - if os.path.sep in file_name: - display_name = os.path.basename(file_name) - # Get icon for the file - icon = icon_provider.icon(QFileInfo(file_name)) + if os.path.sep in display_file_path: + display_name = os.path.basename(display_file_path) + # Get icon for the display file + icon = icon_provider.icon(QFileInfo(display_file_path)) list_item = QListWidgetItem() # Set tooltip with detailed info output_folder = getattr(item, "output_folder", "") - tooltip = ( - f"Path: {file_name}
" + # For plain .txt inputs we don't need to show a separate processing file + show_processing = True + try: + if isinstance(display_file_path, str) and display_file_path.lower().endswith('.txt'): + show_processing = False + except Exception: + show_processing = True + + tooltip = f"Input File: {display_file_path}
" + if show_processing and processing_file_path and processing_file_path != display_file_path: + tooltip += f"Processing File: {processing_file_path}
" + tooltip += ( f"Language: {getattr(item, 'lang_code', '')}
" f"Speed: {getattr(item, 'speed', '')}
" f"Voice: {getattr(item, 'voice', '')}
" @@ -246,10 +259,14 @@ class QueueManager(QDialog): ) list_item.setToolTip(tooltip) list_item.setIcon(icon) - list_item.setData(Qt.UserRole, file_name) + # Store both paths for context menu + list_item.setData(Qt.UserRole, { + 'display_path': display_file_path, + 'processing_path': processing_file_path + }) # Use custom widget for display char_count = getattr(item, "total_char_count", 0) - widget = QueueListItemWidget(file_name, char_count) + widget = QueueListItemWidget(display_file_path, char_count) self.listwidget.addItem(list_item) self.listwidget.setItemWidget(list_item, widget) self.update_button_states() @@ -372,6 +389,7 @@ class QueueManager(QDialog): item = QueueItem() item.file_name = file_path + item.save_base_path = file_path # For .txt files, processing and save paths are the same for attr, value in current_attrs.items(): setattr(item, attr, value) # Read file content and calculate total_char_count using calculate_text_length @@ -405,6 +423,8 @@ class QueueManager(QDialog): == getattr(item, "total_char_count", None) and getattr(queued_item, "replace_single_newlines", False) == getattr(item, "replace_single_newlines", False) + and getattr(queued_item, "save_base_path", None) + == getattr(item, "save_base_path", None) ): is_duplicate = True break @@ -473,42 +493,117 @@ class QueueManager(QDialog): from PyQt5.QtWidgets import QMessageBox item = selected_items[0] - file_path = item.data(Qt.UserRole) + paths = item.data(Qt.UserRole) + if isinstance(paths, dict): + file_path = paths.get('display_path', paths.get('processing_path', '')) + else: + file_path = paths # Fallback for old format + + # Find the queue item for q in self.queue: - if q.file_name == file_path: - if not os.path.exists(q.file_name): + if (getattr(q, "save_base_path", None) == file_path or + q.file_name == file_path): + target_path = getattr(q, "save_base_path", None) or q.file_name + if not os.path.exists(target_path): QMessageBox.warning( self, "File Not Found", f"The file does not exist." ) return - QDesktopServices.openUrl(QUrl.fromLocalFile(q.file_name)) + QDesktopServices.openUrl(QUrl.fromLocalFile(target_path)) break open_file_action.triggered.connect(open_file) menu.addAction(open_file_action) # Add Go to folder action - go_to_folder_action = QAction("Go to folder", self) + # If the queued item represents a converted document (markdown, pdf, epub) + # show two actions: Go to processed file (the cached .txt) and Go to input file (original source) + item = selected_items[0] + paths = item.data(Qt.UserRole) + if isinstance(paths, dict): + display_path = paths.get('display_path', '') + processing_path = paths.get('processing_path', '') + else: + display_path = paths + processing_path = paths - def go_to_folder(): - from PyQt5.QtWidgets import QMessageBox + doc_exts = ('.md', '.markdown', '.pdf', '.epub') + is_document_input = ( + isinstance(display_path, str) and display_path.lower().endswith(doc_exts) + ) or ( + isinstance(processing_path, str) and processing_path.lower().endswith(doc_exts) + ) - item = selected_items[0] - file_path = item.data(Qt.UserRole) + from PyQt5.QtWidgets import QMessageBox + + def open_folder_for(path_label: str): + # path_label should be either 'display' or 'processing' + p = display_path if path_label == 'display' else processing_path + if not p: + QMessageBox.warning(self, "File Not Found", "Path is not available.") + return + # If the stored path is the display path (original) but the actual file may be + # stored on the queue object differently, try to resolve via the queue entry. + target_path = None for q in self.queue: - if q.file_name == file_path: - if not os.path.exists(q.file_name): - QMessageBox.warning( - self, "File Not Found", f"The file does not exist." - ) - return - folder = os.path.dirname(q.file_name) - if os.path.exists(folder): - QDesktopServices.openUrl(QUrl.fromLocalFile(folder)) + if getattr(q, 'save_base_path', None) == display_path or q.file_name == display_path: + if path_label == 'display': + target_path = getattr(q, 'save_base_path', None) or q.file_name + else: + target_path = q.file_name break + if getattr(q, 'save_base_path', None) == processing_path or q.file_name == processing_path: + if path_label == 'display': + target_path = getattr(q, 'save_base_path', None) or q.file_name + else: + target_path = q.file_name + break + # Fallback to the raw path if resolution failed + if not target_path: + target_path = p - go_to_folder_action.triggered.connect(go_to_folder) - menu.addAction(go_to_folder_action) + if not os.path.exists(target_path): + QMessageBox.warning(self, "File Not Found", f"The file does not exist: {target_path}") + return + folder = os.path.dirname(target_path) + if os.path.exists(folder): + QDesktopServices.openUrl(QUrl.fromLocalFile(folder)) + + if is_document_input: + processed_action = QAction("Go to processed file", self) + processed_action.triggered.connect(lambda: open_folder_for('processing')) + menu.addAction(processed_action) + + input_action = QAction("Go to input file", self) + input_action.triggered.connect(lambda: open_folder_for('display')) + menu.addAction(input_action) + else: + # Default behavior for non-document inputs: single "Go to folder" action + go_to_folder_action = QAction("Go to folder", self) + + def go_to_folder(): + item = selected_items[0] + paths = item.data(Qt.UserRole) + if isinstance(paths, dict): + file_path = paths.get('display_path', paths.get('processing_path', '')) + else: + file_path = paths # Fallback for old format + # Find the queue item + for q in self.queue: + if (getattr(q, "save_base_path", None) == file_path or q.file_name == file_path): + target_path = getattr(q, "save_base_path", None) or q.file_name + if not os.path.exists(target_path): + QMessageBox.warning( + self, "File Not Found", f"The file does not exist." + ) + return + folder = os.path.dirname(target_path) + if os.path.exists(folder): + QDesktopServices.openUrl(QUrl.fromLocalFile(folder)) + break + + go_to_folder_action.triggered.connect(go_to_folder) + menu.addAction(go_to_folder_action) elif len(selected_items) > 1: remove_action = QAction(f"Remove selected ({len(selected_items)})", self) diff --git a/abogen/queued_item.py b/abogen/queued_item.py index 9c9fd00..f8c9ece 100644 --- a/abogen/queued_item.py +++ b/abogen/queued_item.py @@ -14,3 +14,4 @@ class QueuedItem: output_format: str total_char_count: int replace_single_newlines: bool = False + save_base_path: str = None