This commit is contained in:
Yuvi9587
2025-07-11 01:24:12 -07:00
parent fa198c41c1
commit bcf26bea20
5 changed files with 482 additions and 293 deletions

View File

@@ -78,6 +78,7 @@ class PostProcessorWorker:
creator_download_folder_ignore_words =None ,
manga_global_file_counter_ref =None ,
use_date_prefix_for_subfolder=False,
keep_in_post_duplicates=False,
session_file_path=None,
session_lock=None,
):
@@ -130,6 +131,7 @@ class PostProcessorWorker:
self .scan_content_for_images =scan_content_for_images
self .creator_download_folder_ignore_words =creator_download_folder_ignore_words
self.use_date_prefix_for_subfolder = use_date_prefix_for_subfolder
self.keep_in_post_duplicates = keep_in_post_duplicates
self.session_file_path = session_file_path
self.session_lock = session_lock
if self .compress_images and Image is None :
@@ -555,58 +557,175 @@ class PostProcessorWorker:
final_total_for_progress =total_size_bytes if download_successful_flag and total_size_bytes >0 else downloaded_size_bytes
self ._emit_signal ('file_progress',api_original_filename ,(downloaded_size_bytes ,final_total_for_progress ))
if self .check_cancel ()or (skip_event and skip_event .is_set ())or (self .pause_event and self .pause_event .is_set ()and not download_successful_flag ):
self .logger (f" ⚠️ Download process interrupted for {api_original_filename }.")
if downloaded_part_file_path and os .path .exists (downloaded_part_file_path ):
try :os .remove (downloaded_part_file_path )
except OSError :pass
return 0 ,1 ,filename_to_save_in_main_path ,was_original_name_kept_flag ,FILE_DOWNLOAD_STATUS_SKIPPED ,None
# Rescue download if an IncompleteRead error occurred but the file is complete
if (not download_successful_flag and
isinstance(last_exception_for_retry_later, http.client.IncompleteRead) and
total_size_bytes > 0 and downloaded_part_file_path and os.path.exists(downloaded_part_file_path)):
try:
actual_size = os.path.getsize(downloaded_part_file_path)
if actual_size == total_size_bytes:
self.logger(f" ✅ Rescued '{api_original_filename}': IncompleteRead error occurred, but file size matches. Proceeding with save.")
download_successful_flag = True
# The hash must be recalculated now that we've verified the file
md5_hasher = hashlib.md5()
with open(downloaded_part_file_path, 'rb') as f_verify:
for chunk in iter(lambda: f_verify.read(8192), b""): # Read in chunks
md5_hasher.update(chunk)
calculated_file_hash = md5_hasher.hexdigest()
except Exception as rescue_exc:
self.logger(f" ⚠️ Failed to rescue file despite matching size. Error: {rescue_exc}")
if not download_successful_flag :
self .logger (f" Download failed for '{api_original_filename }' after {max_retries +1 } attempts.")
if self.check_cancel() or (skip_event and skip_event.is_set()) or (self.pause_event and self.pause_event.is_set() and not download_successful_flag):
self.logger(f" ⚠️ Download process interrupted for {api_original_filename}.")
if downloaded_part_file_path and os.path.exists(downloaded_part_file_path):
try: os.remove(downloaded_part_file_path)
except OSError: pass
return 0, 1, filename_to_save_in_main_path, was_original_name_kept_flag, FILE_DOWNLOAD_STATUS_SKIPPED, None
# This logic block now correctly handles all outcomes: success, failure, or rescued.
if download_successful_flag:
# --- This is the success path ---
if self._check_pause(f"Post-download hash check for '{api_original_filename}'"):
return 0, 1, filename_to_save_in_main_path, was_original_name_kept_flag, FILE_DOWNLOAD_STATUS_SKIPPED, None
is_actually_incomplete_read =False
if isinstance (last_exception_for_retry_later ,http .client .IncompleteRead ):
is_actually_incomplete_read =True
elif hasattr (last_exception_for_retry_later ,'__cause__')and isinstance (last_exception_for_retry_later .__cause__ ,http .client .IncompleteRead ):
is_actually_incomplete_read =True
with self.downloaded_file_hashes_lock:
if calculated_file_hash in self.downloaded_file_hashes:
self.logger(f" -> Skip Saving Duplicate (Hash Match): '{api_original_filename}' (Hash: {calculated_file_hash[:8]}...).")
with self.downloaded_files_lock: self.downloaded_files.add(filename_to_save_in_main_path)
if downloaded_part_file_path and os.path.exists(downloaded_part_file_path):
try: os.remove(downloaded_part_file_path)
except OSError as e_rem: self.logger(f" -> Failed to remove .part file for hash duplicate: {e_rem}")
return 0, 1, filename_to_save_in_main_path, was_original_name_kept_flag, FILE_DOWNLOAD_STATUS_SKIPPED, None
elif last_exception_for_retry_later is not None :
str_exc =str (last_exception_for_retry_later ).lower ()
effective_save_folder = target_folder_path
filename_after_styling_and_word_removal = filename_to_save_in_main_path
if "incompleteread"in str_exc or (isinstance (last_exception_for_retry_later ,tuple )and any ("incompleteread"in str (arg ).lower ()for arg in last_exception_for_retry_later if isinstance (arg ,(str ,Exception )))):
is_actually_incomplete_read =True
try:
os.makedirs(effective_save_folder, exist_ok=True)
except OSError as e:
self.logger(f" ❌ Critical error creating directory '{effective_save_folder}': {e}. Skipping file '{api_original_filename}'.")
if downloaded_part_file_path and os.path.exists(downloaded_part_file_path):
try: os.remove(downloaded_part_file_path)
except OSError: pass
return 0, 1, api_original_filename, False, FILE_DOWNLOAD_STATUS_SKIPPED, None
if is_actually_incomplete_read :
self .logger (f" Marking '{api_original_filename }' for potential retry later due to IncompleteRead.")
retry_later_details ={
'file_info':file_info ,
'target_folder_path':target_folder_path ,
'headers':headers ,
'original_post_id_for_log':original_post_id_for_log ,
'post_title':post_title ,
'file_index_in_post':file_index_in_post ,
'num_files_in_this_post':num_files_in_this_post ,
'forced_filename_override':filename_to_save_in_main_path ,
'manga_mode_active_for_file':self .manga_mode_active ,
'manga_filename_style_for_file':self .manga_filename_style ,
data_to_write_io = None
filename_after_compression = filename_after_styling_and_word_removal
is_img_for_compress_check = is_image(api_original_filename)
if is_img_for_compress_check and self.compress_images and Image and downloaded_size_bytes > (1.5 * 1024 * 1024):
# ... (This block for image compression remains the same)
self .logger (f" Compressing '{api_original_filename }' ({downloaded_size_bytes /(1024 *1024 ):.2f} MB)...")
if self ._check_pause (f"Image compression for '{api_original_filename }'"):return 0 ,1 ,filename_to_save_in_main_path ,was_original_name_kept_flag ,FILE_DOWNLOAD_STATUS_SKIPPED ,None
img_content_for_pillow =None
try :
with open (downloaded_part_file_path ,'rb')as f_img_in :
img_content_for_pillow =BytesIO (f_img_in .read ())
with Image .open (img_content_for_pillow )as img_obj :
if img_obj .mode =='P':img_obj =img_obj .convert ('RGBA')
elif img_obj .mode not in ['RGB','RGBA','L']:img_obj =img_obj .convert ('RGB')
compressed_output_io =BytesIO ()
img_obj .save (compressed_output_io ,format ='WebP',quality =80 ,method =4 )
compressed_size =compressed_output_io .getbuffer ().nbytes
if compressed_size <downloaded_size_bytes *0.9 :
self .logger (f" Compression success: {compressed_size /(1024 *1024 ):.2f} MB.")
data_to_write_io =compressed_output_io
data_to_write_io .seek (0 )
base_name_orig ,_ =os .path .splitext (filename_after_compression )
filename_after_compression =base_name_orig +'.webp'
self .logger (f" Updated filename (compressed): {filename_after_compression }")
else :
self .logger (f" Compression skipped: WebP not significantly smaller.")
if compressed_output_io :compressed_output_io .close ()
except Exception as comp_e :
self .logger (f"❌ Compression failed for '{api_original_filename }': {comp_e }. Saving original.")
finally :
if img_content_for_pillow :img_content_for_pillow .close ()
final_filename_on_disk = filename_after_compression
temp_base, temp_ext = os.path.splitext(final_filename_on_disk)
suffix_counter = 1
while os.path.exists(os.path.join(effective_save_folder, final_filename_on_disk)):
final_filename_on_disk = f"{temp_base}_{suffix_counter}{temp_ext}"
suffix_counter += 1
if final_filename_on_disk != filename_after_compression:
self.logger(f" Applied numeric suffix in '{os.path.basename(effective_save_folder)}': '{final_filename_on_disk}' (was '{filename_after_compression}')")
if self._check_pause(f"File saving for '{final_filename_on_disk}'"):
return 0, 1, final_filename_on_disk, was_original_name_kept_flag, FILE_DOWNLOAD_STATUS_SKIPPED, None
final_save_path = os.path.join(effective_save_folder, final_filename_on_disk)
try:
if data_to_write_io:
with open(final_save_path, 'wb') as f_out:
time.sleep(0.05)
f_out.write(data_to_write_io.getvalue())
if downloaded_part_file_path and os.path.exists(downloaded_part_file_path):
try:
os.remove(downloaded_part_file_path)
except OSError as e_rem:
self.logger(f" -> Failed to remove .part after compression: {e_rem}")
else:
if downloaded_part_file_path and os.path.exists(downloaded_part_file_path):
time.sleep(0.1)
os.rename(downloaded_part_file_path, final_save_path)
else:
raise FileNotFoundError(f"Original .part file not found for saving: {downloaded_part_file_path}")
with self.downloaded_file_hashes_lock: self.downloaded_file_hashes.add(calculated_file_hash)
with self.downloaded_files_lock: self.downloaded_files.add(filename_to_save_in_main_path)
final_filename_saved_for_return = final_filename_on_disk
self.logger(f"✅ Saved: '{final_filename_saved_for_return}' (from '{api_original_filename}', {downloaded_size_bytes / (1024 * 1024):.2f} MB) in '{os.path.basename(effective_save_folder)}'")
downloaded_file_details = {
'disk_filename': final_filename_saved_for_return,
'post_title': post_title,
'post_id': original_post_id_for_log,
'upload_date_str': self.post.get('published') or self.post.get('added') or "N/A",
'download_timestamp': time.time(),
'download_path': effective_save_folder,
'service': self.service,
'user_id': self.user_id,
'api_original_filename': api_original_filename,
'folder_context_name': folder_context_name_for_history or os.path.basename(effective_save_folder)
}
return 0 ,1 ,filename_to_save_in_main_path ,was_original_name_kept_flag ,FILE_DOWNLOAD_STATUS_FAILED_RETRYABLE_LATER ,retry_later_details
else :
self .logger (f" Marking '{api_original_filename }' as permanently failed for this session.")
permanent_failure_details ={
'file_info':file_info ,
'target_folder_path':target_folder_path ,
'headers':headers ,
'original_post_id_for_log':original_post_id_for_log ,
'post_title':post_title ,
'file_index_in_post':file_index_in_post ,
'num_files_in_this_post':num_files_in_this_post ,
'forced_filename_override':filename_to_save_in_main_path ,
}
return 0 ,1 ,filename_to_save_in_main_path ,was_original_name_kept_flag ,FILE_DOWNLOAD_STATUS_FAILED_PERMANENTLY_THIS_SESSION ,permanent_failure_details
if self ._check_pause (f"Post-download hash check for '{api_original_filename }'"):return 0 ,1 ,filename_to_save_in_main_path ,was_original_name_kept_flag ,FILE_DOWNLOAD_STATUS_SKIPPED ,None
self._emit_signal('file_successfully_downloaded', downloaded_file_details)
time.sleep(0.05)
return 1, 0, final_filename_saved_for_return, was_original_name_kept_flag, FILE_DOWNLOAD_STATUS_SUCCESS, None
except Exception as save_err:
self.logger(f"->>Save Fail for '{final_filename_on_disk}': {save_err}")
if os.path.exists(final_save_path):
try: os.remove(final_save_path)
except OSError: self.logger(f" -> Failed to remove partially saved file: {final_save_path}")
return 0, 1, final_filename_saved_for_return, was_original_name_kept_flag, FILE_DOWNLOAD_STATUS_SKIPPED, None
finally:
if data_to_write_io and hasattr(data_to_write_io, 'close'):
data_to_write_io.close()
else:
# --- This is the failure path ---
self.logger(f"❌ Download failed for '{api_original_filename}' after {max_retries + 1} attempts.")
is_actually_incomplete_read = False
if isinstance(last_exception_for_retry_later, http.client.IncompleteRead):
is_actually_incomplete_read = True
elif hasattr(last_exception_for_retry_later, '__cause__') and isinstance(last_exception_for_retry_later.__cause__, http.client.IncompleteRead):
is_actually_incomplete_read = True
elif last_exception_for_retry_later is not None:
str_exc = str(last_exception_for_retry_later).lower()
if "incompleteread" in str_exc or (isinstance(last_exception_for_retry_later, tuple) and any("incompleteread" in str(arg).lower() for arg in last_exception_for_retry_later if isinstance(arg, (str, Exception)))):
is_actually_incomplete_read = True
if is_actually_incomplete_read:
self.logger(f" Marking '{api_original_filename}' for potential retry later due to IncompleteRead.")
retry_later_details = { 'file_info': file_info, 'target_folder_path': target_folder_path, 'headers': headers, 'original_post_id_for_log': original_post_id_for_log, 'post_title': post_title, 'file_index_in_post': file_index_in_post, 'num_files_in_this_post': num_files_in_this_post, 'forced_filename_override': filename_to_save_in_main_path, 'manga_mode_active_for_file': self.manga_mode_active, 'manga_filename_style_for_file': self.manga_filename_style, }
return 0, 1, filename_to_save_in_main_path, was_original_name_kept_flag, FILE_DOWNLOAD_STATUS_FAILED_RETRYABLE_LATER, retry_later_details
else:
self.logger(f" Marking '{api_original_filename}' as permanently failed for this session.")
permanent_failure_details = { 'file_info': file_info, 'target_folder_path': target_folder_path, 'headers': headers, 'original_post_id_for_log': original_post_id_for_log, 'post_title': post_title, 'file_index_in_post': file_index_in_post, 'num_files_in_this_post': num_files_in_this_post, 'forced_filename_override': filename_to_save_in_main_path, }
return 0, 1, filename_to_save_in_main_path, was_original_name_kept_flag, FILE_DOWNLOAD_STATUS_FAILED_PERMANENTLY_THIS_SESSION, permanent_failure_details
with self .downloaded_file_hashes_lock :
if calculated_file_hash in self .downloaded_file_hashes :
self .logger (f" -> Skip Saving Duplicate (Hash Match): '{api_original_filename }' (Hash: {calculated_file_hash [:8 ]}...).")
@@ -1207,16 +1326,26 @@ class PostProcessorWorker:
return 0 ,0 ,[],[],[],None
files_to_download_info_list =[]
processed_original_filenames_in_this_post =set ()
for file_info in all_files_from_post_api :
current_api_original_filename =file_info .get ('_original_name_for_log')
if current_api_original_filename in processed_original_filenames_in_this_post :
self .logger (f" -> Skip Duplicate Original Name (within post {post_id }): '{current_api_original_filename }' already processed/listed for this post.")
total_skipped_this_post +=1
else :
files_to_download_info_list .append (file_info )
if current_api_original_filename :
processed_original_filenames_in_this_post .add (current_api_original_filename )
if not files_to_download_info_list :
if self.keep_in_post_duplicates:
# If we keep duplicates, just add every file to the list to be processed.
# The downstream hash check and rename-on-collision logic will handle them.
files_to_download_info_list.extend(all_files_from_post_api)
self.logger(f" 'Keep Duplicates' is on. All {len(all_files_from_post_api)} files from post will be processed.")
else:
# This is the original logic that skips duplicates by name within a post.
for file_info in all_files_from_post_api:
current_api_original_filename = file_info.get('_original_name_for_log')
if current_api_original_filename in processed_original_filenames_in_this_post:
self.logger(f" -> Skip Duplicate Original Name (within post {post_id}): '{current_api_original_filename}' already processed/listed for this post.")
total_skipped_this_post += 1
else:
files_to_download_info_list.append(file_info)
if current_api_original_filename:
processed_original_filenames_in_this_post.add(current_api_original_filename)
if not files_to_download_info_list:
self .logger (f" All files for post {post_id } were duplicate original names or skipped earlier.")
return 0 ,total_skipped_this_post ,[],[],[],None
@@ -1366,12 +1495,24 @@ class PostProcessorWorker:
with open(self.session_file_path, 'r', encoding='utf-8') as f:
session_data = json.load(f)
# Modify in memory
if not isinstance(session_data.get('download_state', {}).get('processed_post_ids'), list):
if 'download_state' not in session_data:
session_data['download_state'] = {}
if 'download_state' not in session_data:
session_data['download_state'] = {}
# Add processed ID
if not isinstance(session_data['download_state'].get('processed_post_ids'), list):
session_data['download_state']['processed_post_ids'] = []
session_data['download_state']['processed_post_ids'].append(self.post.get('id'))
# Add any permanent failures from this worker to the session file
if permanent_failures_this_post:
if not isinstance(session_data['download_state'].get('permanently_failed_files'), list):
session_data['download_state']['permanently_failed_files'] = []
# To avoid duplicates if the same post is somehow re-processed
existing_failed_urls = {f.get('file_info', {}).get('url') for f in session_data['download_state']['permanently_failed_files']}
for failure in permanent_failures_this_post:
if failure.get('file_info', {}).get('url') not in existing_failed_urls:
session_data['download_state']['permanently_failed_files'].append(failure)
# Write to temp file and then atomically replace
temp_file_path = self.session_file_path + ".tmp"
with open(temp_file_path, 'w', encoding='utf-8') as f_tmp:
@@ -1460,6 +1601,7 @@ class DownloadThread (QThread ):
scan_content_for_images =False ,
creator_download_folder_ignore_words =None ,
use_date_prefix_for_subfolder=False,
keep_in_post_duplicates=False,
cookie_text ="",
session_file_path=None,
session_lock=None,
@@ -1513,6 +1655,7 @@ class DownloadThread (QThread ):
self .scan_content_for_images =scan_content_for_images
self .creator_download_folder_ignore_words =creator_download_folder_ignore_words
self.use_date_prefix_for_subfolder = use_date_prefix_for_subfolder
self.keep_in_post_duplicates = keep_in_post_duplicates
self .manga_global_file_counter_ref =manga_global_file_counter_ref
self.session_file_path = session_file_path
self.session_lock = session_lock
@@ -1646,6 +1789,7 @@ class DownloadThread (QThread ):
use_cookie =self .use_cookie ,
manga_date_file_counter_ref =self .manga_date_file_counter_ref ,
use_date_prefix_for_subfolder=self.use_date_prefix_for_subfolder,
keep_in_post_duplicates=self.keep_in_post_duplicates,
creator_download_folder_ignore_words =self .creator_download_folder_ignore_words ,
session_file_path=self.session_file_path,
session_lock=self.session_lock,

View File

@@ -4,7 +4,7 @@ import sys
# --- PyQt5 Imports ---
from PyQt5.QtCore import QUrl, QSize, Qt
from PyQt5.QtGui import QIcon
from PyQt5.QtGui import QIcon, QDesktopServices
from PyQt5.QtWidgets import (
QApplication, QDialog, QHBoxLayout, QLabel, QPushButton, QVBoxLayout,
QStackedWidget, QScrollArea, QFrame, QWidget

View File

@@ -241,7 +241,7 @@ class DownloaderApp (QWidget ):
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.setWindowTitle("Kemono Downloader v6.0.0")
self.init_ui()
self._connect_signals()
self.log_signal.emit(" Local API server functionality has been removed.")
@@ -274,6 +274,8 @@ class DownloaderApp (QWidget ):
'use_subfolder_per_post_checkbox': 'use_post_subfolders',
'use_multithreading_checkbox': 'use_multithreading',
'external_links_checkbox': 'show_external_links',
'keep_duplicates_checkbox': 'keep_in_post_duplicates',
'date_prefix_checkbox': 'use_date_prefix_for_subfolder',
'manga_mode_checkbox': 'manga_mode_active',
'scan_content_images_checkbox': 'scan_content_for_images',
'use_cookie_checkbox': 'use_cookie',
@@ -342,6 +344,12 @@ class DownloaderApp (QWidget ):
if "ui_settings" not in session_data or "download_state" not in session_data:
raise ValueError("Invalid session file structure.")
failed_files_from_session = session_data.get('download_state', {}).get('permanently_failed_files', [])
if failed_files_from_session:
self.permanently_failed_files_for_dialog.clear()
self.permanently_failed_files_for_dialog.extend(failed_files_from_session)
self.log_signal.emit(f" Restored {len(failed_files_from_session)} failed file entries from the previous session.")
self.interrupted_session_data = session_data
self.log_signal.emit(" Incomplete download session found. UI updated for restore.")
self._prepare_ui_for_restore()
@@ -422,11 +430,13 @@ class DownloaderApp (QWidget ):
self.pause_btn.setEnabled(True)
self.pause_btn.clicked.connect(self.restore_download)
self.pause_btn.setToolTip(self._tr("restore_download_button_tooltip", "Click to restore the interrupted download."))
self.cancel_btn.setEnabled(True)
self.cancel_btn.setText(self._tr("cancel_button_text", "❌ Cancel & Reset UI"))
self.cancel_btn.setEnabled(False) # Nothing to cancel yet
self.cancel_btn.setToolTip(self._tr("cancel_button_tooltip", "Click to cancel the ongoing download/extraction process and reset the UI fields (preserving URL and Directory)."))
# --- START: CORRECTED CANCEL BUTTON LOGIC ---
self.cancel_btn.setText(self._tr("discard_session_button_text", "🗑️ Discard Session"))
self.cancel_btn.setEnabled(True)
self.cancel_btn.clicked.connect(self._clear_session_and_reset_ui)
self.cancel_btn.setToolTip(self._tr("discard_session_tooltip", "Click to discard the interrupted session and reset the UI."))
elif is_download_active:
# State: Downloading / Paused
self.download_btn.setText(self._tr("start_download_button_text", "⬇️ Start Download"))
@@ -1206,6 +1216,11 @@ class DownloaderApp (QWidget ):
self .compress_images_checkbox .setToolTip ("Compress images > 1.5MB to WebP format (requires Pillow).")
row1_layout .addWidget (self .compress_images_checkbox )
self.keep_duplicates_checkbox = QCheckBox("Keep Duplicates")
self.keep_duplicates_checkbox.setChecked(False)
self.keep_duplicates_checkbox.setToolTip("If checked, downloads all files from a post even if they have the same name.\nUnique files will be renamed with a suffix; identical files will still be skipped by hash.")
row1_layout.addWidget(self.keep_duplicates_checkbox)
row1_layout .addStretch (1 )
checkboxes_group_layout .addLayout (row1_layout )
@@ -3578,6 +3593,7 @@ class DownloaderApp (QWidget ):
self .retryable_failed_files_info .clear ()
self .permanently_failed_files_for_dialog .clear ()
self._update_error_button_count()
manga_date_file_counter_ref_for_thread =None
if manga_mode and self .manga_filename_style ==STYLE_DATE_BASED and not extract_links_only :
@@ -3627,6 +3643,9 @@ class DownloaderApp (QWidget ):
if not extract_links_only :
log_messages .append (f" Subfolders: {'Enabled'if use_subfolders else 'Disabled'}")
if use_subfolders and self.use_subfolder_per_post_checkbox.isChecked():
use_date_prefix = self.date_prefix_checkbox.isChecked() if hasattr(self, 'date_prefix_checkbox') else False
log_messages.append(f" ↳ Date Prefix for Post Subfolders: {'Enabled' if use_date_prefix else 'Disabled'}")
if use_subfolders :
if custom_folder_name_cleaned :log_messages .append (f" Custom Folder (Post): '{custom_folder_name_cleaned }'")
if actual_filters_to_use_for_run :
@@ -3636,14 +3655,15 @@ class DownloaderApp (QWidget ):
log_messages .append (f" Folder Naming: Automatic (based on title/known names)")
log_messages .extend ([
f" File Type Filter: {user_selected_filter_text } (Backend processing as: {backend_filter_mode })",
f" Skip Archives: {'.zip'if effective_skip_zip else ''}{', 'if effective_skip_zip and effective_skip_rar else ''}{'.rar'if effective_skip_rar else ''}{'None (Archive Mode)'if backend_filter_mode =='archive'else ('None'if not (effective_skip_zip or effective_skip_rar )else '')}",
f" Skip Words (posts/files): {', '.join (skip_words_list )if skip_words_list else 'None'}",
f" Skip Words Scope: {current_skip_words_scope .capitalize ()}",
f" Remove Words from Filename: {', '.join (remove_from_filename_words_list )if remove_from_filename_words_list else 'None'}",
f" Compress Images: {'Enabled'if compress_images else 'Disabled'}",
f" Thumbnails Only: {'Enabled'if download_thumbnails else 'Disabled'}"
keep_duplicates = self.keep_duplicates_checkbox.isChecked() if hasattr(self, 'keep_duplicates_checkbox') else False
log_messages.extend([
f" File Type Filter: {user_selected_filter_text} (Backend processing as: {backend_filter_mode})",
f" Keep In-Post Duplicates: {'Enabled' if keep_duplicates else 'Disabled'}",
f" Skip Archives: {'.zip' if effective_skip_zip else ''}{', ' if effective_skip_zip and effective_skip_rar else ''}{'.rar' if effective_skip_rar else ''}{'None (Archive Mode)' if backend_filter_mode == 'archive' else ('None' if not (effective_skip_zip or effective_skip_rar) else '')}",
f" Skip Words Scope: {current_skip_words_scope .capitalize ()}",
f" Remove Words from Filename: {', '.join (remove_from_filename_words_list )if remove_from_filename_words_list else 'None'}",
f" Compress Images: {'Enabled'if compress_images else 'Disabled'}",
f" Thumbnails Only: {'Enabled'if download_thumbnails else 'Disabled'}"
])
log_messages .append (f" Scan Post Content for Images: {'Enabled'if scan_content_for_images else 'Disabled'}")
else :
@@ -3731,6 +3751,7 @@ class DownloaderApp (QWidget ):
'session_lock': self.session_lock,
'creator_download_folder_ignore_words':creator_folder_ignore_words_for_run ,
'use_date_prefix_for_subfolder': self.date_prefix_checkbox.isChecked() if hasattr(self, 'date_prefix_checkbox') else False,
'keep_in_post_duplicates': self.keep_duplicates_checkbox.isChecked() if hasattr(self, 'keep_duplicates_checkbox') else False,
}
args_template ['override_output_dir']=override_output_dir
@@ -3830,6 +3851,7 @@ class DownloaderApp (QWidget ):
dialog .exec_ ()
def _handle_retry_from_error_dialog (self ,selected_files_to_retry ):
self ._start_failed_files_retry_session (files_to_retry_list =selected_files_to_retry )
self._update_error_button_count()
def _handle_retryable_file_failure (self ,list_of_retry_details ):
"""Appends details of files that failed but might be retryable later."""
@@ -3841,6 +3863,7 @@ class DownloaderApp (QWidget ):
if list_of_permanent_failure_details :
self .permanently_failed_files_for_dialog .extend (list_of_permanent_failure_details )
self .log_signal .emit (f" {len (list_of_permanent_failure_details )} file(s) from single-thread download marked as permanently failed for this session.")
self._update_error_button_count()
def _submit_post_to_worker_pool (self ,post_data_item ,worker_args_template ,num_file_dl_threads_for_each_worker ,emitter_for_worker ,ppw_expected_keys ,ppw_optional_keys_with_defaults ):
"""Helper to prepare and submit a single post processing task to the thread pool."""
@@ -4129,6 +4152,7 @@ class DownloaderApp (QWidget ):
'manga_filename_style',
'manga_date_prefix',
'use_date_prefix_for_subfolder',
'keep_in_post_duplicates',
'manga_global_file_counter_ref',
'creator_download_folder_ignore_words',
'session_file_path',
@@ -4242,6 +4266,7 @@ class DownloaderApp (QWidget ):
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 )
self._update_error_button_count()
self ._add_to_history_candidates (history_data_from_worker )
with self .downloaded_files_lock :
self .download_counter +=downloaded_files_from_future
@@ -4955,6 +4980,7 @@ class DownloaderApp (QWidget ):
# --- Reset UI and all state ---
self.log_signal.emit("🔄 Resetting application state to defaults...")
self._reset_ui_to_defaults()
self._load_saved_download_location()
self.main_log_output.clear()
self.external_log_output.clear()
if self.missed_character_log_output:
@@ -4989,6 +5015,7 @@ class DownloaderApp (QWidget ):
self.only_links_log_display_mode = LOG_DISPLAY_LINKS
self.mega_download_log_preserved_once = False
self.permanently_failed_files_for_dialog.clear()
self._update_error_button_count()
self.favorite_download_scope = FAVORITE_SCOPE_SELECTED_LOCATION
self._update_favorite_scope_button_text()
self.retryable_failed_files_info.clear()
@@ -5024,7 +5051,6 @@ class DownloaderApp (QWidget ):
"""Resets all UI elements and relevant state to their default values."""
# Clear all text fields
self.link_input.clear()
self.dir_input.clear()
self.custom_folder_input.clear()
self.character_input.clear()
self.skip_words_input.clear()
@@ -5207,6 +5233,19 @@ class DownloaderApp (QWidget ):
self .multipart_toggle_button .setText (self ._tr ("multipart_off_button_text","Multi-part: OFF"))
self .multipart_toggle_button .setToolTip (self ._tr ("multipart_off_button_tooltip","Tooltip for multipart OFF"))
def _update_error_button_count(self):
"""Updates the Error button text to show the count of failed files."""
if not hasattr(self, 'error_btn'):
return
count = len(self.permanently_failed_files_for_dialog)
base_text = self._tr("error_button_text", "Error")
if count > 0:
self.error_btn.setText(f"({count}) {base_text}")
else:
self.error_btn.setText(base_text)
def _toggle_multipart_mode (self ):
if not self .allow_multipart_download_setting :
msg_box =QMessageBox (self )