From c8b77fb0d7f04c4d92b3aa342cd22b3e7c4b2a55 Mon Sep 17 00:00:00 2001 From: Yuvi9587 <114073886+Yuvi9587@users.noreply.github.com> Date: Sat, 5 Jul 2025 06:02:21 +0530 Subject: [PATCH] Commit --- main.py | 29 ++- src/config/constants.py | 3 + src/core/workers.py | 11 +- src/ui/assets.py | 21 ++- src/ui/main_window.py | 401 ++++++++++++++++++++++------------------ 5 files changed, 259 insertions(+), 206 deletions(-) diff --git a/main.py b/main.py index a954c1f..7462e02 100644 --- a/main.py +++ b/main.py @@ -9,24 +9,26 @@ from PyQt5.QtWidgets import QApplication, QDialog from PyQt5.QtCore import QCoreApplication # --- Local Application Imports --- -# These imports reflect the new, organized project structure. from src.ui.main_window import DownloaderApp from src.ui.dialogs.TourDialog import TourDialog from src.config.constants import CONFIG_ORGANIZATION_NAME, CONFIG_APP_NAME_MAIN +# --- Define APP_BASE_DIR globally and make available early --- +if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'): + APP_BASE_DIR = sys._MEIPASS +else: + APP_BASE_DIR = os.path.abspath(os.path.dirname(__file__)) + +# Optional: Set a global variable or pass it into modules if needed +# Or re-export it via constants.py for cleaner imports def handle_uncaught_exception(exc_type, exc_value, exc_traceback): """ Handles uncaught exceptions by logging them to a file for easier debugging, especially for bundled applications. """ - # Determine the base directory for logging - if getattr(sys, 'frozen', False): - base_dir_for_log = os.path.dirname(sys.executable) - else: - base_dir_for_log = os.path.dirname(os.path.abspath(__file__)) - - log_dir = os.path.join(base_dir_for_log, "logs") + # Use APP_BASE_DIR to determine logging location + log_dir = os.path.join(APP_BASE_DIR, "logs") log_file_path = os.path.join(log_dir, "uncaught_exceptions.log") try: @@ -57,41 +59,35 @@ def main(): qt_app = QApplication(sys.argv) - # Create the main application window from its new module + # Create the main application window downloader_app_instance = DownloaderApp() # --- Window Sizing and Positioning --- - # Logic moved from the old main.py to set an appropriate initial size primary_screen = QApplication.primaryScreen() if not primary_screen: - # Fallback for systems with no primary screen detected downloader_app_instance.resize(1024, 768) else: available_geo = primary_screen.availableGeometry() screen_width = available_geo.width() screen_height = available_geo.height() - # Define minimums and desired ratios min_app_width, min_app_height = 960, 680 desired_width_ratio, desired_height_ratio = 0.80, 0.85 app_width = max(min_app_width, int(screen_width * desired_width_ratio)) app_height = max(min_app_height, int(screen_height * desired_height_ratio)) - # Ensure the window is not larger than the screen app_width = min(app_width, screen_width) app_height = min(app_height, screen_height) downloader_app_instance.resize(app_width, app_height) - # Show the main window and center it + # Show and center the main window downloader_app_instance.show() if hasattr(downloader_app_instance, '_center_on_screen'): downloader_app_instance._center_on_screen() # --- First-Run Welcome Tour --- - # Check if the tour should be shown and run it. - # This static method call keeps the logic clean and contained. if TourDialog.should_show_tour(): tour_dialog = TourDialog(parent_app=downloader_app_instance) tour_dialog.exec_() @@ -102,7 +98,6 @@ def main(): sys.exit(exit_code) except SystemExit: - # Allow sys.exit() to work as intended pass except Exception as e: print("--- CRITICAL APPLICATION STARTUP ERROR ---") diff --git a/src/config/constants.py b/src/config/constants.py index 0386606..14bf2bf 100644 --- a/src/config/constants.py +++ b/src/config/constants.py @@ -9,6 +9,7 @@ STYLE_ORIGINAL_NAME = "original_name" STYLE_DATE_BASED = "date_based" STYLE_DATE_POST_TITLE = "date_post_title" STYLE_POST_TITLE_GLOBAL_NUMBERING = "post_title_global_numbering" +STYLE_POST_ID = "post_id" # Add this line MANGA_DATE_PREFIX_DEFAULT = "" # --- Download Scopes --- @@ -94,6 +95,7 @@ FOLDER_NAME_STOP_WORDS = { "me", "my", "net", "not", "of", "on", "or", "org", "our", "s", "she", "so", "the", "their", "they", "this", "to", "ve", "was", "we", "were", "with", "www", "you", "your", + # add more according to need } # Additional words to ignore specifically for creator-level downloads @@ -107,4 +109,5 @@ CREATOR_DOWNLOAD_DEFAULT_FOLDER_IGNORE_WORDS = { "oct", "october", "nov", "november", "dec", "december", "mon", "monday", "tue", "tuesday", "wed", "wednesday", "thu", "thursday", "fri", "friday", "sat", "saturday", "sun", "sunday" + # add more according to need } diff --git a/src/core/workers.py b/src/core/workers.py index db7c613..9f4ac2a 100644 --- a/src/core/workers.py +++ b/src/core/workers.py @@ -167,6 +167,7 @@ class PostProcessorWorker: if self .dynamic_filter_holder : return self .dynamic_filter_holder .get_filters () return self .filter_character_list_objects_initial + def _download_single_file (self ,file_info ,target_folder_path ,headers ,original_post_id_for_log ,skip_event , post_title ="",file_index_in_post =0 ,num_files_in_this_post =1 , manga_date_file_counter_ref =None , @@ -273,6 +274,15 @@ class PostProcessorWorker: self .logger (f"⚠️ Manga Title+GlobalNum Mode: Counter ref not provided or malformed for '{api_original_filename }'. Using original. Ref: {manga_global_file_counter_ref }") filename_to_save_in_main_path =cleaned_original_api_filename self .logger (f"⚠️ Manga mode (Title+GlobalNum Style Fallback): Using cleaned original filename '{filename_to_save_in_main_path }' for post {original_post_id_for_log }.") + elif self.manga_filename_style == STYLE_POST_ID: + if original_post_id_for_log and original_post_id_for_log != 'unknown_id': + base_name = str(original_post_id_for_log) + # Always append the file index for consistency (e.g., xxxxxx_0, xxxxxx_1) + filename_to_save_in_main_path = f"{base_name}_{file_index_in_post}{original_ext}" + else: + # Fallback if post_id is somehow not available + self.logger(f"⚠️ Manga mode (Post ID Style): Post ID missing. Using cleaned original filename '{cleaned_original_api_filename}'.") + filename_to_save_in_main_path = cleaned_original_api_filename elif self .manga_filename_style ==STYLE_DATE_POST_TITLE : published_date_str =self .post .get ('published') added_date_str =self .post .get ('added') @@ -717,7 +727,6 @@ class PostProcessorWorker: if data_to_write_io and hasattr (data_to_write_io ,'close'): data_to_write_io .close () - def process (self ): if self ._check_pause (f"Post processing for ID {self .post .get ('id','N/A')}"):return 0 ,0 ,[],[],[],None if self .check_cancel ():return 0 ,0 ,[],[],[],None diff --git a/src/ui/assets.py b/src/ui/assets.py index 628c718..58e719e 100644 --- a/src/ui/assets.py +++ b/src/ui/assets.py @@ -22,13 +22,17 @@ def get_app_icon_object(): if _app_icon_cache and not _app_icon_cache.isNull(): return _app_icon_cache + # Declare a single variable to hold the base directory path. + app_base_dir = "" + # Determine the project's base directory, whether running from source or as a bundled app if getattr(sys, 'frozen', False): - # The application is frozen (e.g., with PyInstaller) - base_dir = os.path.dirname(sys.executable) + # The application is frozen (e.g., with PyInstaller). + # The base directory is the one containing the executable. + app_base_dir = os.path.dirname(sys.executable) else: - # The application is running from a .py file - # This path navigates up from src/ui/ to the project root + # The application is running from a .py file. + # This path navigates up from src/ui/assets.py to the project root. app_base_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')) icon_path = os.path.join(app_base_dir, 'assets', 'Kemono.ico') @@ -36,7 +40,14 @@ def get_app_icon_object(): if os.path.exists(icon_path): _app_icon_cache = QIcon(icon_path) else: + # If the icon isn't found, especially in a frozen app, check the _MEIPASS directory as a fallback. + if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'): + fallback_icon_path = os.path.join(sys._MEIPASS, 'assets', 'Kemono.ico') + if os.path.exists(fallback_icon_path): + _app_icon_cache = QIcon(fallback_icon_path) + return _app_icon_cache + print(f"Warning: Application icon not found at {icon_path}") _app_icon_cache = QIcon() # Return an empty icon as a fallback - return _app_icon_cache + return _app_icon_cache \ No newline at end of file diff --git a/src/ui/main_window.py b/src/ui/main_window.py index f6b06b0..8cce81f 100644 --- a/src/ui/main_window.py +++ b/src/ui/main_window.py @@ -92,171 +92,177 @@ class DownloaderApp (QWidget ): file_progress_signal =pyqtSignal (str ,object ) - def __init__ (self ): - super ().__init__ () - self .settings =QSettings (CONFIG_ORGANIZATION_NAME ,CONFIG_APP_NAME_MAIN ) - if getattr (sys ,'frozen',False ): - self .app_base_dir =os .path .dirname (sys .executable ) - else : + def __init__(self): + super().__init__() + self.settings = QSettings(CONFIG_ORGANIZATION_NAME, CONFIG_APP_NAME_MAIN) + + # --- CORRECT PATH DEFINITION --- + # This block correctly determines the application's base directory whether + # it's running from source or as a frozen executable. + if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'): + # Path for PyInstaller one-file bundle + self.app_base_dir = os.path.dirname(sys.executable) + else: + # Path for running from source code self.app_base_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')) - self .config_file =os .path .join (self .app_base_dir ,"appdata","Known.txt") - self .download_thread =None - self .thread_pool =None - self .cancellation_event =threading .Event () - self.session_file_path = os.path.join(self.app_base_dir, "appdata","session.json") + # All file paths will now correctly use the single, correct app_base_dir + self.config_file = os.path.join(self.app_base_dir, "appdata", "Known.txt") + self.session_file_path = os.path.join(self.app_base_dir, "appdata", "session.json") + self.persistent_history_file = os.path.join(self.app_base_dir, "appdata", "download_history.json") + + self.download_thread = None + self.thread_pool = None + self.cancellation_event = threading.Event() self.session_lock = threading.Lock() self.interrupted_session_data = None self.is_restore_pending = False - self .external_link_download_thread =None - self .pause_event =threading .Event () - self .active_futures =[] - self .total_posts_to_process =0 - self .dynamic_character_filter_holder =DynamicFilterHolder () - self .processed_posts_count =0 - self .creator_name_cache ={} - self .log_signal .emit (f"ℹ️ App base directory: {self .app_base_dir }") + self.external_link_download_thread = None + self.pause_event = threading.Event() + self.active_futures = [] + self.total_posts_to_process = 0 + self.dynamic_character_filter_holder = DynamicFilterHolder() + self.processed_posts_count = 0 + self.creator_name_cache = {} + self.log_signal.emit(f"ℹ️ App base directory: {self.app_base_dir}") + self.log_signal.emit(f"ℹ️ Persistent history file path set to: {self.persistent_history_file}") - self.app_base_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')) - self.persistent_history_file = os.path.join(self.app_base_dir, "appdata", "download_history.json") - self .last_downloaded_files_details =deque (maxlen =3 ) - self .download_history_candidates =deque (maxlen =8 ) - self .log_signal .emit (f"ℹ️ Persistent history file path set to: {self .persistent_history_file }") - self .final_download_history_entries =[] - self .favorite_download_queue =deque () - self .is_processing_favorites_queue =False - self .download_counter =0 - self .favorite_download_queue =deque () - self .permanently_failed_files_for_dialog =[] - self .last_link_input_text_for_queue_sync ="" - self .is_fetcher_thread_running =False - self ._restart_pending =False - self .is_processing_favorites_queue =False - self .download_history_log =deque (maxlen =50 ) - self .skip_counter =0 - self .all_kept_original_filenames =[] - self .cancellation_message_logged_this_session =False - self .favorite_scope_toggle_button =None - self .favorite_download_scope =FAVORITE_SCOPE_SELECTED_LOCATION + # --- The rest of your __init__ method continues from here --- + self.last_downloaded_files_details = deque(maxlen=3) + self.download_history_candidates = deque(maxlen=8) + self.final_download_history_entries = [] + self.favorite_download_queue = deque() + self.is_processing_favorites_queue = False + self.download_counter = 0 + self.permanently_failed_files_for_dialog = [] + self.last_link_input_text_for_queue_sync = "" + self.is_fetcher_thread_running = False + self._restart_pending = False + self.download_history_log = deque(maxlen=50) + self.skip_counter = 0 + self.all_kept_original_filenames = [] + self.cancellation_message_logged_this_session = False + self.favorite_scope_toggle_button = None + self.favorite_download_scope = FAVORITE_SCOPE_SELECTED_LOCATION + self.manga_mode_checkbox = None + self.selected_cookie_filepath = None + self.retryable_failed_files_info = [] + self.is_paused = False + self.worker_to_gui_queue = queue.Queue() + self.gui_update_timer = QTimer(self) + self.actual_gui_signals = PostProcessorSignals() + self.worker_signals = PostProcessorSignals() + self.prompt_mutex = QMutex() + self._add_character_response = None + self._original_scan_content_tooltip = ("If checked, the downloader will scan the HTML content of posts for image URLs (from tags or direct links).\n" + "now This includes resolving relative paths from tags to full URLs.\n" + "Relative paths in tags (e.g., /data/image.jpg) will be resolved to full URLs.\n" + "Useful for cases where images are in the post description but not in the API's file/attachment list.") + self.downloaded_files = set() + self.downloaded_files_lock = threading.Lock() + self.downloaded_file_hashes = set() + self.downloaded_file_hashes_lock = threading.Lock() + self.show_external_links = False + self.external_link_queue = deque() + self._is_processing_external_link_queue = False + self._current_link_post_title = None + self.extracted_links_cache = [] + self.manga_rename_toggle_button = None + self.favorite_mode_checkbox = None + self.url_or_placeholder_stack = None + self.url_input_widget = None + self.url_placeholder_widget = None + self.favorite_action_buttons_widget = None + self.favorite_mode_artists_button = None + self.favorite_mode_posts_button = None + self.standard_action_buttons_widget = None + self.bottom_action_buttons_stack = None + self.main_log_output = None + self.external_log_output = None + self.log_splitter = None + self.main_splitter = None + self.reset_button = None + self.progress_log_label = None + self.log_verbosity_toggle_button = None + self.missed_character_log_output = None + self.log_view_stack = None + self.current_log_view = 'progress' + self.link_search_input = None + self.link_search_button = None + self.export_links_button = None + self.radio_only_links = None + self.radio_only_archives = None + self.missed_title_key_terms_count = {} + self.missed_title_key_terms_examples = {} + self.logged_summary_for_key_term = set() + self.STOP_WORDS = set(["a", "an", "the", "is", "was", "were", "of", "for", "with", "in", "on", "at", "by", "to", "and", "or", "but", "i", "you", "he", "she", "it", "we", "they", "my", "your", "his", "her", "its", "our", "their", "com", "net", "org", "www"]) + self.already_logged_bold_key_terms = set() + self.missed_key_terms_buffer = [] + self.char_filter_scope_toggle_button = None + self.skip_words_scope = SKIP_SCOPE_POSTS + self.char_filter_scope = CHAR_SCOPE_TITLE + self.manga_filename_style = self.settings.value(MANGA_FILENAME_STYLE_KEY, STYLE_POST_TITLE, type=str) + self.current_theme = self.settings.value(THEME_KEY, "dark", type=str) + self.only_links_log_display_mode = LOG_DISPLAY_LINKS + self.mega_download_log_preserved_once = False + self.allow_multipart_download_setting = False + self.use_cookie_setting = False + self.scan_content_images_setting = self.settings.value(SCAN_CONTENT_IMAGES_KEY, False, type=bool) + self.cookie_text_setting = "" + self.current_selected_language = self.settings.value(LANGUAGE_KEY, "en", type=str) - self .manga_mode_checkbox =None + print(f"ℹ️ Known.txt will be loaded/saved at: {self.config_file}") - self .selected_cookie_filepath =None - self .retryable_failed_files_info =[] + try: + base_path_for_icon = "" + if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'): + base_path_for_icon = sys._MEIPASS + else: + base_path_for_icon = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')) + + icon_path_for_window = os.path.join(base_path_for_icon, 'assets', 'Kemono.ico') + + if os.path.exists(icon_path_for_window): + self.setWindowIcon(QIcon(icon_path_for_window)) + else: + if getattr(sys, 'frozen', False): + executable_dir = os.path.dirname(sys.executable) + fallback_icon_path = os.path.join(executable_dir, 'assets', 'Kemono.ico') + if os.path.exists(fallback_icon_path): + self.setWindowIcon(QIcon(fallback_icon_path)) + else: + self.log_signal.emit(f"⚠️ Main window icon 'assets/Kemono.ico' not found at {icon_path_for_window} or {fallback_icon_path}") + else: + self.log_signal.emit(f"⚠️ Main window icon 'assets/Kemono.ico' not found at {icon_path_for_window}") + except Exception as e_icon_app: + self.log_signal.emit(f"❌ Error setting main window icon in DownloaderApp init: {e_icon_app}") - self .is_paused =False - self .worker_to_gui_queue =queue .Queue () - self .gui_update_timer =QTimer (self ) - self .actual_gui_signals =PostProcessorSignals () - - self .worker_signals =PostProcessorSignals () - self .prompt_mutex =QMutex () - self ._add_character_response =None - - self ._original_scan_content_tooltip =("If checked, the downloader will scan the HTML content of posts for image URLs (from tags or direct links).\n" - "now This includes resolving relative paths from tags to full URLs.\n" - "Relative paths in tags (e.g., /data/image.jpg) will be resolved to full URLs.\n" - "Useful for cases where images are in the post description but not in the API's file/attachment list.") - - self .downloaded_files =set () - self .downloaded_files_lock =threading .Lock () - self .downloaded_file_hashes =set () - self .downloaded_file_hashes_lock =threading .Lock () - - self .show_external_links =False - self .external_link_queue =deque () - self ._is_processing_external_link_queue =False - self ._current_link_post_title =None - self .extracted_links_cache =[] - self .manga_rename_toggle_button =None - self .favorite_mode_checkbox =None - self .url_or_placeholder_stack =None - self .url_input_widget =None - self .url_placeholder_widget =None - self .favorite_action_buttons_widget =None - self .favorite_mode_artists_button =None - self .favorite_mode_posts_button =None - self .standard_action_buttons_widget =None - self .bottom_action_buttons_stack =None - self .main_log_output =None - self .external_log_output =None - self .log_splitter =None - self .main_splitter =None - self .reset_button =None - self .progress_log_label =None - self .log_verbosity_toggle_button =None - - self .missed_character_log_output =None - self .log_view_stack =None - self .current_log_view ='progress' - - self .link_search_input =None - self .link_search_button =None - self .export_links_button =None - self .radio_only_links =None - self .radio_only_archives =None - self .missed_title_key_terms_count ={} - self .missed_title_key_terms_examples ={} - self .logged_summary_for_key_term =set () - self .STOP_WORDS =set (["a","an","the","is","was","were","of","for","with","in","on","at","by","to","and","or","but","i","you","he","she","it","we","they","my","your","his","her","its","our","their","com","net","org","www"]) - self .already_logged_bold_key_terms =set () - self .missed_key_terms_buffer =[] - self .char_filter_scope_toggle_button =None - self .skip_words_scope =SKIP_SCOPE_POSTS - self .char_filter_scope =CHAR_SCOPE_TITLE - self .manga_filename_style =self .settings .value (MANGA_FILENAME_STYLE_KEY ,STYLE_POST_TITLE ,type =str ) - self .current_theme =self .settings .value (THEME_KEY ,"dark",type =str ) - self .only_links_log_display_mode =LOG_DISPLAY_LINKS - self .mega_download_log_preserved_once =False - self .allow_multipart_download_setting =False - self .use_cookie_setting =False - self .scan_content_images_setting =self .settings .value (SCAN_CONTENT_IMAGES_KEY ,False ,type =bool ) - self .cookie_text_setting ="" - self .current_selected_language =self .settings .value (LANGUAGE_KEY ,"en",type =str ) - - print (f"ℹ️ Known.txt will be loaded/saved at: {self .config_file }") - try : - if getattr (sys ,'frozen',False )and hasattr (sys ,'_MEIPASS'): - - base_dir_for_icon =sys ._MEIPASS - else : - app_base_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')) - icon_path_for_window =os .path .join (app_base_dir ,'assets','Kemono.ico') - if os .path .exists (icon_path_for_window ): - self .setWindowIcon (QIcon (icon_path_for_window )) - else : - self .log_signal .emit (f"⚠️ Main window icon 'assets/Kemono.ico' not found at {icon_path_for_window } (tried in DownloaderApp init)") - except Exception as e_icon_app : - self .log_signal .emit (f"❌ Error setting main window icon in DownloaderApp init: {e_icon_app }") - - self .url_label_widget =None - self .download_location_label_widget =None - - self .remove_from_filename_label_widget =None - self .skip_words_label_widget =None - - self .setWindowTitle ("Kemono Downloader v5.5.0") - - self .init_ui () - self ._connect_signals () - self .log_signal .emit ("ℹ️ Local API server functionality has been removed.") - self .log_signal .emit ("ℹ️ 'Skip Current File' button has been removed.") - if hasattr (self ,'character_input'): - self .character_input .setToolTip (self ._tr ("character_input_tooltip","Enter character names (comma-separated)...")) - self .log_signal .emit (f"ℹ️ Manga filename style loaded: '{self .manga_filename_style }'") - self .log_signal .emit (f"ℹ️ Skip words scope loaded: '{self .skip_words_scope }'") - self .log_signal .emit (f"ℹ️ Character filter scope set to default: '{self .char_filter_scope }'") - self .log_signal .emit (f"ℹ️ Multi-part download defaults to: {'Enabled'if self .allow_multipart_download_setting else 'Disabled'}") - self .log_signal .emit (f"ℹ️ Cookie text defaults to: Empty on launch") - self .log_signal .emit (f"ℹ️ 'Use Cookie' setting defaults to: Disabled on launch") - self .log_signal .emit (f"ℹ️ Scan post content for images defaults to: {'Enabled'if self .scan_content_images_setting else 'Disabled'}") - self .log_signal .emit (f"ℹ️ Application language loaded: '{self .current_selected_language .upper ()}' (UI may not reflect this yet).") - self ._retranslate_main_ui () - self ._load_persistent_history () - self ._load_saved_download_location () - self._update_button_states_and_connections() # Initial button state setup + self.url_label_widget = None + self.download_location_label_widget = None + self.remove_from_filename_label_widget = None + self.skip_words_label_widget = None + self.setWindowTitle("Kemono Downloader v5.5.0") + self.init_ui() + self._connect_signals() + self.log_signal.emit("ℹ️ Local API server functionality has been removed.") + self.log_signal.emit("ℹ️ 'Skip Current File' button has been removed.") + if hasattr(self, 'character_input'): + self.character_input.setToolTip(self._tr("character_input_tooltip", "Enter character names (comma-separated)...")) + self.log_signal.emit(f"ℹ️ Manga filename style loaded: '{self.manga_filename_style}'") + self.log_signal.emit(f"ℹ️ Skip words scope loaded: '{self.skip_words_scope}'") + self.log_signal.emit(f"ℹ️ Character filter scope set to default: '{self.char_filter_scope}'") + self.log_signal.emit(f"ℹ️ Multi-part download defaults to: {'Enabled' if self.allow_multipart_download_setting else 'Disabled'}") + self.log_signal.emit(f"ℹ️ Cookie text defaults to: Empty on launch") + self.log_signal.emit(f"ℹ️ 'Use Cookie' setting defaults to: Disabled on launch") + self.log_signal.emit(f"ℹ️ Scan post content for images defaults to: {'Enabled' if self.scan_content_images_setting else 'Disabled'}") + self.log_signal.emit(f"ℹ️ Application language loaded: '{self.current_selected_language.upper()}' (UI may not reflect this yet).") + self._retranslate_main_ui() + self._load_persistent_history() + self._load_saved_download_location() + self._update_button_states_and_connections() self._check_for_interrupted_session() + def get_checkbox_map(self): """Returns a mapping of checkbox attribute names to their corresponding settings key.""" return { @@ -851,20 +857,40 @@ class DownloaderApp (QWidget ): self .character_list .addItems ([entry ["name"]for entry in KNOWN_NAMES ]) - def save_known_names (self ): - global KNOWN_NAMES - try : - with open (self .config_file ,'w',encoding ='utf-8')as f : - for entry in KNOWN_NAMES : - if entry ["is_group"]: - f .write (f"({', '.join (sorted (entry ['aliases'],key =str .lower ))})\n") - else : - f .write (entry ["name"]+'\n') - if hasattr (self ,'log_signal'):self .log_signal .emit (f"💾 Saved {len (KNOWN_NAMES )} known entries to {self .config_file }") - except Exception as e : - log_msg =f"❌ Error saving config '{self .config_file }': {e }" - if hasattr (self ,'log_signal'):self .log_signal .emit (log_msg ) - QMessageBox .warning (self ,"Config Save Error",f"Could not save list to {self .config_file }:\n{e }") + def save_known_names(self): + """ + Saves the current list of known names (KNOWN_NAMES) to the config file. + This version includes a fix to ensure the destination directory exists + before attempting to write the file, preventing crashes in new installations. + """ + global KNOWN_NAMES + try: + # --- FIX STARTS HERE --- + # Get the directory path from the full file path. + config_dir = os.path.dirname(self.config_file) + # Create the directory if it doesn't exist. 'exist_ok=True' prevents + # an error if the directory is already there. + os.makedirs(config_dir, exist_ok=True) + # --- FIX ENDS HERE --- + + with open(self.config_file, 'w', encoding='utf-8') as f: + for entry in KNOWN_NAMES: + if entry["is_group"]: + # For groups, write the aliases in a sorted, comma-separated format inside parentheses. + f.write(f"({', '.join(sorted(entry['aliases'], key=str.lower))})\n") + else: + # For single entries, write the name on its own line. + f.write(entry["name"] + '\n') + + if hasattr(self, 'log_signal'): + self.log_signal.emit(f"💾 Saved {len(KNOWN_NAMES)} known entries to {self.config_file}") + + except Exception as e: + # If any error occurs during saving, log it and show a warning popup. + log_msg = f"❌ Error saving config '{self.config_file}': {e}" + if hasattr(self, 'log_signal'): + self.log_signal.emit(log_msg) + QMessageBox.warning(self, "Config Save Error", f"Could not save list to {self.config_file}:\n{e}") def closeEvent (self ,event ): self .save_known_names () @@ -1526,25 +1552,28 @@ class DownloaderApp (QWidget ): self .final_download_history_entries =[] self ._save_persistent_history () - def _save_persistent_history (self ): + + def _save_persistent_history(self): """Saves download history to a persistent file.""" - self .log_signal .emit (f"📜 Attempting to save history to: {self .persistent_history_file }") - try : - history_dir =os .path .dirname (self .persistent_history_file ) - self .log_signal .emit (f" History directory: {history_dir }") - if not os .path .exists (history_dir ): - os .makedirs (history_dir ,exist_ok =True ) - self .log_signal .emit (f" Created history directory: {history_dir }") + self.log_signal.emit(f"📜 Attempting to save history to: {self.persistent_history_file}") + try: + history_dir = os.path.dirname(self.persistent_history_file) + self.log_signal.emit(f" History directory: {history_dir}") + if not os.path.exists(history_dir): + os.makedirs(history_dir, exist_ok=True) + self.log_signal.emit(f" Created history directory: {history_dir}") history_data = { "last_downloaded_files": list(self.last_downloaded_files_details), "first_processed_posts": self.final_download_history_entries } - with open (self .persistent_history_file ,'w',encoding ='utf-8')as f : - json .dump (history_data ,f ,indent =2 ) - self .log_signal .emit (f"✅ Saved {len (self .final_download_history_entries )} history entries to: {self .persistent_history_file }") - except Exception as e : - self .log_signal .emit (f"❌ Error saving persistent history to {self .persistent_history_file }: {e }") + with open(self.persistent_history_file, 'w', encoding='utf-8') as f: + json.dump(history_data, f, indent=2) + self.log_signal.emit(f"✅ Saved {len(self.final_download_history_entries)} history entries to: {self.persistent_history_file}") + except Exception as e: + self.log_signal.emit(f"❌ Error saving persistent history to {self.persistent_history_file}: {e}") + + def _load_creator_name_cache_from_json (self ): """Loads creator id-name-service mappings from creators.json into self.creator_name_cache.""" self .log_signal .emit ("ℹ️ Attempting to load creators.json for creator name cache.") @@ -2751,7 +2780,9 @@ class DownloaderApp (QWidget ): elif self .manga_filename_style ==STYLE_DATE_BASED : self .manga_rename_toggle_button .setText (self ._tr ("manga_style_date_based_text","Name: Date Based")) - + + elif self .manga_filename_style ==STYLE_POST_ID: # Add this block + self .manga_rename_toggle_button .setText (self ._tr ("manga_style_post_id_text","Name: Post ID")) elif self .manga_filename_style ==STYLE_DATE_POST_TITLE : self .manga_rename_toggle_button .setText (self ._tr ("manga_style_date_post_title_text","Name: Date + Title")) @@ -2763,6 +2794,8 @@ class DownloaderApp (QWidget ): self .manga_rename_toggle_button .setToolTip ("Click to cycle Manga Filename Style (when Manga Mode is active for a creator feed).") +# In main_window.py + def _toggle_manga_filename_style (self ): current_style =self .manga_filename_style new_style ="" @@ -2775,7 +2808,9 @@ class DownloaderApp (QWidget ): elif current_style ==STYLE_POST_TITLE_GLOBAL_NUMBERING : new_style =STYLE_DATE_BASED elif current_style ==STYLE_DATE_BASED : - new_style =STYLE_POST_TITLE + new_style =STYLE_POST_ID # Change this line + elif current_style ==STYLE_POST_ID: # Add this block + new_style =STYLE_POST_TITLE else : self .log_signal .emit (f"⚠️ Unknown current manga filename style: {current_style }. Resetting to default ('{STYLE_POST_TITLE }').") new_style =STYLE_POST_TITLE