diff --git a/downloader_utils.py b/downloader_utils.py index 06c2ff7..0192d13 100644 --- a/downloader_utils.py +++ b/downloader_utils.py @@ -1689,7 +1689,48 @@ class PostProcessorWorker : if not self .extract_links_only and self .use_post_subfolders : cleaned_post_title_for_sub =clean_folder_name (post_title ) - determined_post_save_path_for_history =os .path .join (determined_post_save_path_for_history ,cleaned_post_title_for_sub ) + post_id_for_fallback = self.post.get('id', 'unknown_id') # Ensure post_id is available + + # Fallback to a more unique name if the cleaned title is generic + if not cleaned_post_title_for_sub or cleaned_post_title_for_sub == "untitled_folder": + self.logger(f" ⚠️ Post title '{post_title}' resulted in a generic subfolder name. Using 'post_{post_id_for_fallback}' as base.") + original_cleaned_post_title_for_sub = f"post_{post_id_for_fallback}" + else: + original_cleaned_post_title_for_sub = cleaned_post_title_for_sub + + # Path before adding the post-specific subfolder + base_path_for_post_subfolder = determined_post_save_path_for_history + + suffix_counter = 0 # 0 for no suffix, 1 for _1, etc. + final_post_subfolder_name = "" + + while True: + if suffix_counter == 0: + name_candidate = original_cleaned_post_title_for_sub + else: + name_candidate = f"{original_cleaned_post_title_for_sub}_{suffix_counter}" + + potential_post_subfolder_path = os.path.join(base_path_for_post_subfolder, name_candidate) + + try: + os.makedirs(potential_post_subfolder_path, exist_ok=False) + final_post_subfolder_name = name_candidate + if suffix_counter > 0: # Log only if a suffix was actually needed and used + self.logger(f" Post subfolder name conflict: Using '{final_post_subfolder_name}' instead of '{original_cleaned_post_title_for_sub}' to avoid mixing posts.") + break + except FileExistsError: + suffix_counter += 1 + if suffix_counter > 100: # Safety break + self.logger(f" ⚠️ Exceeded 100 attempts to find unique subfolder name for '{original_cleaned_post_title_for_sub}'. Using UUID.") + final_post_subfolder_name = f"{original_cleaned_post_title_for_sub}_{uuid.uuid4().hex[:8]}" + os.makedirs(os.path.join(base_path_for_post_subfolder, final_post_subfolder_name), exist_ok=True) # Create with exist_ok=True as a last resort + break + except OSError as e_mkdir: + self.logger(f" ❌ Error creating directory '{potential_post_subfolder_path}': {e_mkdir}. Files for this post might be saved in parent or fail.") + final_post_subfolder_name = original_cleaned_post_title_for_sub # Fallback + break + + determined_post_save_path_for_history = os.path.join(base_path_for_post_subfolder, final_post_subfolder_name) if not self .extract_links_only and self .use_subfolders and self .skip_words_list : if self ._check_pause (f"Folder keyword skip check for post {post_id }"):return 0 ,num_potential_files_in_post ,[],[],[],None @@ -1952,8 +1993,8 @@ class PostProcessorWorker : if self .use_subfolders and target_base_folder_name_for_instance : current_path_for_file_instance =os .path .join (current_path_for_file_instance ,target_base_folder_name_for_instance ) if self .use_post_subfolders : - cleaned_title_for_subfolder_instance =clean_folder_name (post_title ) - current_path_for_file_instance =os .path .join (current_path_for_file_instance ,cleaned_title_for_subfolder_instance ) + # Use the final_post_subfolder_name determined earlier, which includes suffix if needed + current_path_for_file_instance =os .path .join (current_path_for_file_instance ,final_post_subfolder_name ) manga_date_counter_to_pass =self .manga_date_file_counter_ref if self .manga_mode_active and self .manga_filename_style ==STYLE_DATE_BASED else None manga_global_counter_to_pass =self .manga_global_file_counter_ref if self .manga_mode_active and self .manga_filename_style ==STYLE_POST_TITLE_GLOBAL_NUMBERING else None @@ -2021,6 +2062,22 @@ class PostProcessorWorker : } if self .check_cancel ():self .logger (f" Post {post_id } processing interrupted/cancelled."); else :self .logger (f" Post {post_id } Summary: Downloaded={total_downloaded_this_post }, Skipped Files={total_skipped_this_post }") + + # Cleanup: Remove empty post-specific subfolder if created and no files were downloaded + if not self.extract_links_only and self.use_post_subfolders and total_downloaded_this_post == 0: + # determined_post_save_path_for_history at this point holds the full path to the post-specific subfolder + # if self.use_post_subfolders was true and it was applied. + # base_path_for_post_subfolder was the path *before* the post-specific segment. + # final_post_subfolder_name was the segment itself. + # So, determined_post_save_path_for_history is the correct path to check. + path_to_check_for_emptiness = determined_post_save_path_for_history + try: + if os.path.isdir(path_to_check_for_emptiness) and not os.listdir(path_to_check_for_emptiness): + self.logger(f" 🗑️ Removing empty post-specific subfolder: '{path_to_check_for_emptiness}'") + os.rmdir(path_to_check_for_emptiness) + except OSError as e_rmdir: + self.logger(f" ⚠️ Could not remove empty post-specific subfolder '{path_to_check_for_emptiness}': {e_rmdir}") + return total_downloaded_this_post ,total_skipped_this_post ,kept_original_filenames_for_log ,retryable_failures_this_post ,permanent_failures_this_post ,history_data_for_this_post class DownloadThread (QThread ): progress_signal =pyqtSignal (str ) diff --git a/languages.py b/languages.py index e35d9e6..05d1ac6 100644 --- a/languages.py +++ b/languages.py @@ -296,6 +296,22 @@ Output: '2023-01-15_ChapterOne.jpg', '2023-01-15_ChapterOne_1.png'""", "download_external_links_dialog_title":"Download Selected External Links", "select_all_button_text":"Select All", "deselect_all_button_text":"Deselect All", +"deselect_all_button_text":"Deselect All", # Existing, but good to have for context +"settings_download_group_title": "Download Settings", +"settings_save_path_button": "Save Current Download Path", +"settings_save_path_tooltip": "Save the current 'Download Location' from the main window for future sessions.", +"settings_save_path_success_title": "Path Saved", +"settings_save_path_success_message": "Download location '{path}' saved successfully.", +"settings_save_path_invalid_title": "Invalid Path", +"settings_save_path_invalid_message": "The path '{path}' is not a valid directory. Please select a valid directory first.", +"settings_save_path_empty_title": "Empty Path", +"settings_save_path_empty_message": "Download location cannot be empty. Please select a path first.", +"settings_save_all_settings_button_text": "Save All Settings", +"settings_save_all_settings_button_tooltip": "Save all current application settings (download path, checkboxes, inputs, etc.).", +"settings_all_saved_success_title": "Settings Saved", +"settings_all_saved_success_message": "All application settings saved successfully.", +"settings_all_saved_error_title": "Save Error", +"settings_all_saved_error_message": "Could not save all application settings. Check the log for details.", "cookie_browse_button_tooltip":"Browse for a cookie file (Netscape format, typically cookies.txt).\nThis will be used if 'Use Cookie' is checked and the text field above is empty." , "page_range_label_text":"Page Range:", diff --git a/main.py b/main.py index b38b5c9..6c55b04 100644 --- a/main.py +++ b/main.py @@ -178,6 +178,7 @@ CHAR_FILTER_SCOPE_KEY ="charFilterScopeV1" THEME_KEY ="currentThemeV2" SCAN_CONTENT_IMAGES_KEY ="scanContentForImagesV1" LANGUAGE_KEY ="currentLanguageV1" +DOWNLOAD_LOCATION_KEY ="downloadLocationV1" CONFIRM_ADD_ALL_ACCEPTED =1 FAVORITE_SCOPE_SELECTED_LOCATION ="selected_location" @@ -719,6 +720,11 @@ class FutureSettingsDialog (QDialog ): layout .addWidget (self .language_group_box ) + self .save_path_button =QPushButton () + self .save_path_button .clicked .connect (self ._save_download_path ) + layout .addWidget (self .save_path_button ) + + layout .addStretch (1 ) self .ok_button =QPushButton () @@ -740,6 +746,9 @@ class FutureSettingsDialog (QDialog ): self .language_label .setText (self ._tr ("language_label","Language:")) self ._update_theme_toggle_button_text () self ._populate_language_combo_box () + if hasattr (self ,'save_path_button'): + self .save_path_button .setText (self ._tr ("settings_save_path_button","Save Current Download Path")) + self .save_path_button .setToolTip (self ._tr ("settings_save_path_tooltip","Save the current 'Download Location' from the main window for future sessions.")) self .ok_button .setText (self ._tr ("ok_button","OK")) def _update_theme_toggle_button_text (self ): if self .parent_app .current_theme =="dark": @@ -812,6 +821,29 @@ class FutureSettingsDialog (QDialog ): if msg_box .clickedButton ()==restart_button : self .parent_app ._request_restart_application () + def _save_download_path (self ): + if self .parent_app and hasattr (self .parent_app ,'dir_input')and self .parent_app .dir_input : + current_path =self .parent_app .dir_input .text ().strip () + if current_path : + if os .path .isdir (current_path ): + self .parent_app .settings .setValue (DOWNLOAD_LOCATION_KEY ,current_path ) + self .parent_app .settings .sync () + QMessageBox .information (self , + self ._tr ("settings_save_path_success_title","Path Saved"), + self ._tr ("settings_save_path_success_message",f"Download location '{current_path }' saved successfully.")) + if hasattr (self .parent_app ,'log_signal'): + self .parent_app .log_signal .emit (f"💾 Download location '{current_path }' saved.") + else : + QMessageBox .warning (self , + self ._tr ("settings_save_path_invalid_title","Invalid Path"), + self ._tr ("settings_save_path_invalid_message",f"The path '{current_path }' is not a valid directory. Please select a valid directory first.")) + else : + QMessageBox .warning (self , + self ._tr ("settings_save_path_empty_title","Empty Path"), + self ._tr ("settings_save_path_empty_message","Download location cannot be empty. Please select a path first.")) + else : + QMessageBox .critical (self ,"Error","Could not access download path input from main application.") + class EmptyPopupDialog (QDialog ): """A simple empty popup dialog.""" SCOPE_CHARACTERS ="Characters" @@ -1145,8 +1177,6 @@ class EmptyPopupDialog (QDialog ): self .all_creators_data =[] self .progress_bar .setVisible (False );QCoreApplication .processEvents ();return - - self ._filter_list () def _populate_list_widget (self ,creators_to_display ): @@ -1219,16 +1249,8 @@ class EmptyPopupDialog (QDialog ): norm_search_casefolded =unicodedata .normalize ('NFKC',raw_search_input ).casefold ().strip () - - - - - - - scored_matches =[] - parsed_service_from_url ,parsed_user_id_from_url ,_ =extract_post_info (raw_search_input ) if parsed_service_from_url and parsed_user_id_from_url : @@ -2116,8 +2138,24 @@ class KnownNamesFilterDialog (QDialog ): self ._retranslate_ui () self ._populate_list_widget () - self .setMinimumWidth (350 ) - self .setMinimumHeight (400 ) + + screen_geometry =QApplication .primaryScreen ().availableGeometry () + base_width ,base_height =460 ,450 + + + + reference_screen_height =1080 + scale_factor_w =screen_geometry .width ()/1920 + scale_factor_h =screen_geometry .height ()/reference_screen_height + + + + + effective_scale_factor =max (0.75 ,min (scale_factor_h ,1.5 )) + + self .setMinimumSize (int (base_width *effective_scale_factor ),int (base_height *effective_scale_factor )) + self .resize (int (base_width *effective_scale_factor *1.1 ),int (base_height *effective_scale_factor *1.1 )) + if self .parent_app and hasattr (self .parent_app ,'get_dark_theme')and self .parent_app .current_theme =="dark": self .setStyleSheet (self .parent_app .get_dark_theme ()) self .add_button .setDefault (True ) @@ -2137,6 +2175,12 @@ class KnownNamesFilterDialog (QDialog ): self .cancel_button .setText (self ._tr ("fav_posts_cancel_button","Cancel")) def _populate_list_widget (self ,names_to_display =None ): + + + if hasattr (self ,'_items_populated')and self ._items_populated and names_to_display is not None : + + return + self .names_list_widget .clear () current_entries_source =names_to_display if names_to_display is not None else self .all_known_name_entries for entry_obj in current_entries_source : @@ -2145,16 +2189,15 @@ class KnownNamesFilterDialog (QDialog ): item .setCheckState (Qt .Unchecked ) item .setData (Qt .UserRole ,entry_obj ) self .names_list_widget .addItem (item ) + self ._items_populated =True def _filter_list_display (self ): search_text =self .search_input .text ().lower () - if not search_text : - self ._populate_list_widget () - return - filtered_entries =[ - entry_obj for entry_obj in self .all_known_name_entries if search_text in entry_obj ['name'].lower () - ] - self ._populate_list_widget (filtered_entries ) + for i in range (self .names_list_widget .count ()): + item =self .names_list_widget .item (i ) + entry_obj =item .data (Qt .UserRole ) + matches_search =not search_text or search_text in entry_obj ['name'].lower () + item .setHidden (not matches_search ) def _accept_selection_action (self ): self .selected_entries_to_return =[] @@ -3747,6 +3790,7 @@ class DownloaderApp (QWidget ): 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 () def _tr (self ,key ,default_text =""): @@ -3755,6 +3799,18 @@ class DownloaderApp (QWidget ): return get_translation (self .current_selected_language ,key ,default_text ) return default_text + def _load_saved_download_location (self ): + saved_location =self .settings .value (DOWNLOAD_LOCATION_KEY ,"",type =str ) + if saved_location and os .path .isdir (saved_location ): + if hasattr (self ,'dir_input')and self .dir_input : + self .dir_input .setText (saved_location ) + self .log_signal .emit (f"ℹ️ Loaded saved download location: {saved_location }") + else : + self .log_signal .emit (f"⚠️ Found saved download location '{saved_location }', but dir_input not ready.") + elif saved_location : + self .log_signal .emit (f"⚠️ Found saved download location '{saved_location }', but it's not a valid directory. Ignoring.") + + def _initialize_persistent_history_path (self ): documents_path =QStandardPaths .writableLocation (QStandardPaths .DocumentsLocation ) if not documents_path : @@ -5990,7 +6046,7 @@ class DownloaderApp (QWidget ): is_only_archives =self .radio_only_archives and self .radio_only_archives .isChecked () is_only_audio =hasattr (self ,'radio_only_audio')and self .radio_only_audio .isChecked () - can_enable_subfolder_per_post_checkbox =not is_only_links and not is_only_archives + can_enable_subfolder_per_post_checkbox =not is_only_links if self .use_subfolder_per_post_checkbox : self .use_subfolder_per_post_checkbox .setEnabled (can_enable_subfolder_per_post_checkbox ) @@ -7363,8 +7419,8 @@ class DownloaderApp (QWidget ): downloaded_files_from_future ,skipped_files_from_future ,kept_originals_from_future ,retryable_failures_from_post ,permanent_failures_from_post ,history_data_from_worker =result_tuple if history_data_from_worker : self ._add_to_history_candidates (history_data_from_worker ) - if permanent_failures_from_post: - self.permanently_failed_files_for_dialog.extend(permanent_failures_from_post) + if permanent_failures_from_post : + self .permanently_failed_files_for_dialog .extend (permanent_failures_from_post ) self ._add_to_history_candidates (history_data_from_worker ) with self .downloaded_files_lock : self .download_counter +=downloaded_files_from_future