mirror of
https://github.com/Yuvi9587/Kemono-Downloader.git
synced 2025-12-17 15:36:51 +00:00
Commit
This commit is contained in:
parent
3c1b361fc1
commit
191dbc8c62
@ -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 )
|
||||
|
||||
16
languages.py
16
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:",
|
||||
|
||||
96
main.py
96
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 )
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user