mirror of
https://github.com/Yuvi9587/Kemono-Downloader.git
synced 2025-12-29 16:14:44 +00:00
Commit
This commit is contained in:
@@ -72,7 +72,7 @@ CONFIRM_ADD_ALL_CANCEL_DOWNLOAD = 3
|
|||||||
|
|
||||||
# --- File Type Extensions ---
|
# --- File Type Extensions ---
|
||||||
IMAGE_EXTENSIONS = {
|
IMAGE_EXTENSIONS = {
|
||||||
'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff', '.tif', '.webp',
|
'.jpg', '.jpeg', '.jpe', '.png', '.gif', '.bmp', '.tiff', '.tif', '.webp',
|
||||||
'.heic', '.heif', '.svg', '.ico', '.jfif', '.pjpeg', '.pjp', '.avif'
|
'.heic', '.heif', '.svg', '.ico', '.jfif', '.pjpeg', '.pjp', '.avif'
|
||||||
}
|
}
|
||||||
VIDEO_EXTENSIONS = {
|
VIDEO_EXTENSIONS = {
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ class PostProcessorWorker:
|
|||||||
|
|
||||||
def __init__(self, post_data, download_root, known_names,
|
def __init__(self, post_data, download_root, known_names,
|
||||||
filter_character_list, emitter,
|
filter_character_list, emitter,
|
||||||
unwanted_keywords, filter_mode, skip_zip, skip_rar,
|
unwanted_keywords, filter_mode, skip_zip,
|
||||||
use_subfolders, use_post_subfolders, target_post_id_from_initial_url, custom_folder_name,
|
use_subfolders, use_post_subfolders, target_post_id_from_initial_url, custom_folder_name,
|
||||||
compress_images, download_thumbnails, service, user_id, pause_event,
|
compress_images, download_thumbnails, service, user_id, pause_event,
|
||||||
api_url_input, cancellation_event,
|
api_url_input, cancellation_event,
|
||||||
@@ -121,7 +121,6 @@ class PostProcessorWorker:
|
|||||||
self.unwanted_keywords = unwanted_keywords if unwanted_keywords is not None else set()
|
self.unwanted_keywords = unwanted_keywords if unwanted_keywords is not None else set()
|
||||||
self.filter_mode = filter_mode
|
self.filter_mode = filter_mode
|
||||||
self.skip_zip = skip_zip
|
self.skip_zip = skip_zip
|
||||||
self.skip_rar = skip_rar
|
|
||||||
self.use_subfolders = use_subfolders
|
self.use_subfolders = use_subfolders
|
||||||
self.use_post_subfolders = use_post_subfolders
|
self.use_post_subfolders = use_post_subfolders
|
||||||
self.target_post_id_from_initial_url = target_post_id_from_initial_url
|
self.target_post_id_from_initial_url = target_post_id_from_initial_url
|
||||||
@@ -394,13 +393,9 @@ class PostProcessorWorker:
|
|||||||
if not is_audio_type:
|
if not is_audio_type:
|
||||||
self.logger(f" -> Filter Skip: '{api_original_filename}' (Not Audio).")
|
self.logger(f" -> Filter Skip: '{api_original_filename}' (Not Audio).")
|
||||||
return 0, 1, api_original_filename, False, FILE_DOWNLOAD_STATUS_SKIPPED, None
|
return 0, 1, api_original_filename, False, FILE_DOWNLOAD_STATUS_SKIPPED, None
|
||||||
if self.skip_zip and is_zip(api_original_filename):
|
if (self.skip_zip) and is_archive(api_original_filename):
|
||||||
self.logger(f" -> Pref Skip: '{api_original_filename}' (ZIP).")
|
self.logger(f" -> Pref Skip: '{api_original_filename}' (Archive).")
|
||||||
return 0, 1, api_original_filename, False, FILE_DOWNLOAD_STATUS_SKIPPED, None
|
return 0, 1, api_original_filename, False, FILE_DOWNLOAD_STATUS_SKIPPED, None
|
||||||
if self.skip_rar and is_rar(api_original_filename):
|
|
||||||
self.logger(f" -> Pref Skip: '{api_original_filename}' (RAR).")
|
|
||||||
return 0, 1, api_original_filename, False, FILE_DOWNLOAD_STATUS_SKIPPED, None
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
os.makedirs(target_folder_path, exist_ok=True)
|
os.makedirs(target_folder_path, exist_ok=True)
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
@@ -568,7 +563,6 @@ class PostProcessorWorker:
|
|||||||
if self._check_pause(f"Post-download hash check for '{api_original_filename}'"):
|
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
|
return 0, 1, filename_to_save_in_main_path, was_original_name_kept_flag, FILE_DOWNLOAD_STATUS_SKIPPED, None
|
||||||
|
|
||||||
# --- Final Corrected Duplicate Handling Logic ---
|
|
||||||
should_skip = False
|
should_skip = False
|
||||||
with self.downloaded_hash_counts_lock:
|
with self.downloaded_hash_counts_lock:
|
||||||
current_count = self.downloaded_hash_counts.get(calculated_file_hash, 0)
|
current_count = self.downloaded_hash_counts.get(calculated_file_hash, 0)
|
||||||
@@ -695,6 +689,7 @@ class PostProcessorWorker:
|
|||||||
|
|
||||||
|
|
||||||
def process(self):
|
def process(self):
|
||||||
|
|
||||||
result_tuple = (0, 0, [], [], [], None, None)
|
result_tuple = (0, 0, [], [], [], None, None)
|
||||||
try:
|
try:
|
||||||
if self._check_pause(f"Post processing for ID {self.post.get('id', 'N/A')}"):
|
if self._check_pause(f"Post processing for ID {self.post.get('id', 'N/A')}"):
|
||||||
@@ -729,6 +724,7 @@ class PostProcessorWorker:
|
|||||||
effective_unwanted_keywords_for_folder_naming.update(self.creator_download_folder_ignore_words)
|
effective_unwanted_keywords_for_folder_naming.update(self.creator_download_folder_ignore_words)
|
||||||
|
|
||||||
post_content_html = post_data.get('content', '')
|
post_content_html = post_data.get('content', '')
|
||||||
|
if not self.extract_links_only:
|
||||||
self.logger(f"\n--- Processing Post {post_id} ('{post_title[:50]}...') (Thread: {threading.current_thread().name}) ---")
|
self.logger(f"\n--- Processing Post {post_id} ('{post_title[:50]}...') (Thread: {threading.current_thread().name}) ---")
|
||||||
num_potential_files_in_post = len(post_attachments or []) + (1 if post_main_file_info and post_main_file_info.get('path') else 0)
|
num_potential_files_in_post = len(post_attachments or []) + (1 if post_main_file_info and post_main_file_info.get('path') else 0)
|
||||||
|
|
||||||
@@ -1264,9 +1260,6 @@ class PostProcessorWorker:
|
|||||||
else:
|
else:
|
||||||
self.logger(f" ⚠️ Skipping invalid attachment {idx + 1} for post {post_id}: {str(att_info)[:100]}")
|
self.logger(f" ⚠️ Skipping invalid attachment {idx + 1} for post {post_id}: {str(att_info)[:100]}")
|
||||||
|
|
||||||
# --- START: Conditionally de-duplicate files from API response ---
|
|
||||||
# Only de-duplicate by URL if we are in the default hash-skipping mode.
|
|
||||||
# If the user wants to keep everything, we must process all entries from the API.
|
|
||||||
if self.keep_duplicates_mode == DUPLICATE_HANDLING_HASH:
|
if self.keep_duplicates_mode == DUPLICATE_HANDLING_HASH:
|
||||||
unique_files_by_url = {}
|
unique_files_by_url = {}
|
||||||
for file_info in all_files_from_post_api:
|
for file_info in all_files_from_post_api:
|
||||||
@@ -1281,7 +1274,6 @@ class PostProcessorWorker:
|
|||||||
|
|
||||||
if new_count < original_count:
|
if new_count < original_count:
|
||||||
self.logger(f" De-duplicated file list: Removed {original_count - new_count} redundant entries from the API response.")
|
self.logger(f" De-duplicated file list: Removed {original_count - new_count} redundant entries from the API response.")
|
||||||
# --- END: Conditionally de-duplicate files from API response ---
|
|
||||||
|
|
||||||
if self.scan_content_for_images and post_content_html and not self.extract_links_only:
|
if self.scan_content_for_images and post_content_html and not self.extract_links_only:
|
||||||
self.logger(f" Scanning post content for additional image URLs (Post ID: {post_id})...")
|
self.logger(f" Scanning post content for additional image URLs (Post ID: {post_id})...")
|
||||||
@@ -1614,7 +1606,7 @@ class DownloadThread(QThread):
|
|||||||
def __init__(self, api_url_input, output_dir, known_names_copy,
|
def __init__(self, api_url_input, output_dir, known_names_copy,
|
||||||
cancellation_event,
|
cancellation_event,
|
||||||
pause_event, filter_character_list=None, dynamic_character_filter_holder=None,
|
pause_event, filter_character_list=None, dynamic_character_filter_holder=None,
|
||||||
filter_mode='all', skip_zip=True, skip_rar=True,
|
filter_mode='all', skip_zip=True,
|
||||||
use_subfolders=True, use_post_subfolders=False, custom_folder_name=None, compress_images=False,
|
use_subfolders=True, use_post_subfolders=False, custom_folder_name=None, compress_images=False,
|
||||||
download_thumbnails=False, service=None, user_id=None,
|
download_thumbnails=False, service=None, user_id=None,
|
||||||
downloaded_files=None, downloaded_file_hashes=None, downloaded_files_lock=None, downloaded_file_hashes_lock=None,
|
downloaded_files=None, downloaded_file_hashes=None, downloaded_files_lock=None, downloaded_file_hashes_lock=None,
|
||||||
@@ -1654,7 +1646,8 @@ class DownloadThread(QThread):
|
|||||||
text_export_format='txt',
|
text_export_format='txt',
|
||||||
single_pdf_mode=False,
|
single_pdf_mode=False,
|
||||||
project_root_dir=None,
|
project_root_dir=None,
|
||||||
processed_post_ids=None):
|
processed_post_ids=None,
|
||||||
|
start_offset=0):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.api_url_input = api_url_input
|
self.api_url_input = api_url_input
|
||||||
self.output_dir = output_dir
|
self.output_dir = output_dir
|
||||||
@@ -1667,7 +1660,6 @@ class DownloadThread(QThread):
|
|||||||
self.dynamic_filter_holder = dynamic_character_filter_holder
|
self.dynamic_filter_holder = dynamic_character_filter_holder
|
||||||
self.filter_mode = filter_mode
|
self.filter_mode = filter_mode
|
||||||
self.skip_zip = skip_zip
|
self.skip_zip = skip_zip
|
||||||
self.skip_rar = skip_rar
|
|
||||||
self.use_subfolders = use_subfolders
|
self.use_subfolders = use_subfolders
|
||||||
self.use_post_subfolders = use_post_subfolders
|
self.use_post_subfolders = use_post_subfolders
|
||||||
self.custom_folder_name = custom_folder_name
|
self.custom_folder_name = custom_folder_name
|
||||||
@@ -1717,7 +1709,8 @@ class DownloadThread(QThread):
|
|||||||
self.text_export_format = text_export_format
|
self.text_export_format = text_export_format
|
||||||
self.single_pdf_mode = single_pdf_mode
|
self.single_pdf_mode = single_pdf_mode
|
||||||
self.project_root_dir = project_root_dir
|
self.project_root_dir = project_root_dir
|
||||||
self.processed_post_ids = processed_post_ids if processed_post_ids is not None else []
|
self.processed_post_ids_set = set(processed_post_ids) if processed_post_ids is not None else set()
|
||||||
|
self.start_offset = start_offset
|
||||||
|
|
||||||
if self.compress_images and Image is None:
|
if self.compress_images and Image is None:
|
||||||
self.logger("⚠️ Image compression disabled: Pillow library not found (DownloadThread).")
|
self.logger("⚠️ Image compression disabled: Pillow library not found (DownloadThread).")
|
||||||
@@ -1730,7 +1723,9 @@ class DownloadThread(QThread):
|
|||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
"""
|
"""
|
||||||
The main execution method for the single-threaded download process.
|
The main execution method for the download process.
|
||||||
|
This version correctly uses the central `download_from_api` function
|
||||||
|
and explicitly maps all arguments to the PostProcessorWorker to prevent TypeErrors.
|
||||||
"""
|
"""
|
||||||
grand_total_downloaded_files = 0
|
grand_total_downloaded_files = 0
|
||||||
grand_total_skipped_files = 0
|
grand_total_skipped_files = 0
|
||||||
@@ -1749,6 +1744,7 @@ class DownloadThread(QThread):
|
|||||||
worker_signals_obj.worker_finished_signal.connect(lambda result: None)
|
worker_signals_obj.worker_finished_signal.connect(lambda result: None)
|
||||||
|
|
||||||
self.logger(" Starting post fetch (single-threaded download process)...")
|
self.logger(" Starting post fetch (single-threaded download process)...")
|
||||||
|
|
||||||
post_generator = download_from_api(
|
post_generator = download_from_api(
|
||||||
self.api_url_input,
|
self.api_url_input,
|
||||||
logger=self.logger,
|
logger=self.logger,
|
||||||
@@ -1762,109 +1758,105 @@ class DownloadThread(QThread):
|
|||||||
selected_cookie_file=self.selected_cookie_file,
|
selected_cookie_file=self.selected_cookie_file,
|
||||||
app_base_dir=self.app_base_dir,
|
app_base_dir=self.app_base_dir,
|
||||||
manga_filename_style_for_sort_check=self.manga_filename_style if self.manga_mode_active else None,
|
manga_filename_style_for_sort_check=self.manga_filename_style if self.manga_mode_active else None,
|
||||||
# --- FIX: ADDED A COMMA to the line above ---
|
processed_post_ids=self.processed_post_ids_set
|
||||||
processed_post_ids=self.processed_post_ids
|
|
||||||
)
|
)
|
||||||
|
|
||||||
for posts_batch_data in post_generator:
|
for posts_batch_data in post_generator:
|
||||||
if self.isInterruptionRequested():
|
if self.isInterruptionRequested():
|
||||||
was_process_cancelled = True
|
was_process_cancelled = True
|
||||||
break
|
break
|
||||||
|
|
||||||
for individual_post_data in posts_batch_data:
|
for individual_post_data in posts_batch_data:
|
||||||
if self.isInterruptionRequested():
|
if self.isInterruptionRequested():
|
||||||
was_process_cancelled = True
|
was_process_cancelled = True
|
||||||
break
|
break
|
||||||
|
|
||||||
post_processing_worker = PostProcessorWorker(
|
# --- START OF FIX: Explicitly build the arguments dictionary ---
|
||||||
post_data=individual_post_data,
|
# This robustly maps all thread attributes to the correct worker parameters.
|
||||||
download_root=self.output_dir,
|
worker_args = {
|
||||||
known_names=self.known_names,
|
'post_data': individual_post_data,
|
||||||
filter_character_list=self.filter_character_list_objects_initial,
|
'emitter': worker_signals_obj,
|
||||||
dynamic_character_filter_holder=self.dynamic_filter_holder,
|
'download_root': self.output_dir,
|
||||||
unwanted_keywords=self.unwanted_keywords,
|
'known_names': self.known_names,
|
||||||
filter_mode=self.filter_mode,
|
'filter_character_list': self.filter_character_list_objects_initial,
|
||||||
skip_zip=self.skip_zip, skip_rar=self.skip_rar,
|
'dynamic_character_filter_holder': self.dynamic_filter_holder,
|
||||||
use_subfolders=self.use_subfolders, use_post_subfolders=self.use_post_subfolders,
|
'target_post_id_from_initial_url': self.initial_target_post_id,
|
||||||
target_post_id_from_initial_url=self.initial_target_post_id,
|
'num_file_threads': self.num_file_threads_for_worker,
|
||||||
custom_folder_name=self.custom_folder_name,
|
'processed_post_ids': list(self.processed_post_ids_set),
|
||||||
compress_images=self.compress_images, download_thumbnails=self.download_thumbnails,
|
'unwanted_keywords': self.unwanted_keywords,
|
||||||
service=self.service, user_id=self.user_id,
|
'filter_mode': self.filter_mode,
|
||||||
api_url_input=self.api_url_input,
|
'skip_zip': self.skip_zip,
|
||||||
pause_event=self.pause_event,
|
'use_subfolders': self.use_subfolders,
|
||||||
cancellation_event=self.cancellation_event,
|
'use_post_subfolders': self.use_post_subfolders,
|
||||||
emitter=worker_signals_obj,
|
'custom_folder_name': self.custom_folder_name,
|
||||||
downloaded_files=self.downloaded_files,
|
'compress_images': self.compress_images,
|
||||||
downloaded_file_hashes=self.downloaded_file_hashes,
|
'download_thumbnails': self.download_thumbnails,
|
||||||
downloaded_files_lock=self.downloaded_files_lock,
|
'service': self.service,
|
||||||
downloaded_file_hashes_lock=self.downloaded_file_hashes_lock,
|
'user_id': self.user_id,
|
||||||
skip_words_list=self.skip_words_list,
|
'api_url_input': self.api_url_input,
|
||||||
skip_words_scope=self.skip_words_scope,
|
'pause_event': self.pause_event,
|
||||||
show_external_links=self.show_external_links,
|
'cancellation_event': self.cancellation_event,
|
||||||
extract_links_only=self.extract_links_only,
|
'downloaded_files': self.downloaded_files,
|
||||||
num_file_threads=self.num_file_threads_for_worker,
|
'downloaded_file_hashes': self.downloaded_file_hashes,
|
||||||
skip_current_file_flag=self.skip_current_file_flag,
|
'downloaded_files_lock': self.downloaded_files_lock,
|
||||||
manga_mode_active=self.manga_mode_active,
|
'downloaded_file_hashes_lock': self.downloaded_file_hashes_lock,
|
||||||
manga_filename_style=self.manga_filename_style,
|
'skip_words_list': self.skip_words_list,
|
||||||
manga_date_prefix=self.manga_date_prefix,
|
'skip_words_scope': self.skip_words_scope,
|
||||||
char_filter_scope=self.char_filter_scope,
|
'show_external_links': self.show_external_links,
|
||||||
remove_from_filename_words_list=self.remove_from_filename_words_list,
|
'extract_links_only': self.extract_links_only,
|
||||||
allow_multipart_download=self.allow_multipart_download,
|
'skip_current_file_flag': self.skip_current_file_flag,
|
||||||
selected_cookie_file=self.selected_cookie_file,
|
'manga_mode_active': self.manga_mode_active,
|
||||||
app_base_dir=self.app_base_dir,
|
'manga_filename_style': self.manga_filename_style,
|
||||||
cookie_text=self.cookie_text,
|
'char_filter_scope': self.char_filter_scope,
|
||||||
override_output_dir=self.override_output_dir,
|
'remove_from_filename_words_list': self.remove_from_filename_words_list,
|
||||||
manga_global_file_counter_ref=self.manga_global_file_counter_ref,
|
'allow_multipart_download': self.allow_multipart_download,
|
||||||
use_cookie=self.use_cookie,
|
'cookie_text': self.cookie_text,
|
||||||
manga_date_file_counter_ref=self.manga_date_file_counter_ref,
|
'use_cookie': self.use_cookie,
|
||||||
use_date_prefix_for_subfolder=self.use_date_prefix_for_subfolder,
|
'override_output_dir': self.override_output_dir,
|
||||||
keep_in_post_duplicates=self.keep_in_post_duplicates,
|
'selected_cookie_file': self.selected_cookie_file,
|
||||||
keep_duplicates_mode=self.keep_duplicates_mode,
|
'app_base_dir': self.app_base_dir,
|
||||||
keep_duplicates_limit=self.keep_duplicates_limit,
|
'manga_date_prefix': self.manga_date_prefix,
|
||||||
downloaded_hash_counts=self.downloaded_hash_counts,
|
'manga_date_file_counter_ref': self.manga_date_file_counter_ref,
|
||||||
downloaded_hash_counts_lock=self.downloaded_hash_counts_lock,
|
'scan_content_for_images': self.scan_content_for_images,
|
||||||
creator_download_folder_ignore_words=self.creator_download_folder_ignore_words,
|
'creator_download_folder_ignore_words': self.creator_download_folder_ignore_words,
|
||||||
session_file_path=self.session_file_path,
|
'manga_global_file_counter_ref': self.manga_global_file_counter_ref,
|
||||||
session_lock=self.session_lock,
|
'use_date_prefix_for_subfolder': self.use_date_prefix_for_subfolder,
|
||||||
text_only_scope=self.text_only_scope,
|
'keep_in_post_duplicates': self.keep_in_post_duplicates,
|
||||||
text_export_format=self.text_export_format,
|
'keep_duplicates_mode': self.keep_duplicates_mode,
|
||||||
single_pdf_mode=self.single_pdf_mode,
|
'keep_duplicates_limit': self.keep_duplicates_limit,
|
||||||
project_root_dir=self.project_root_dir
|
'downloaded_hash_counts': self.downloaded_hash_counts,
|
||||||
)
|
'downloaded_hash_counts_lock': self.downloaded_hash_counts_lock,
|
||||||
try:
|
'session_file_path': self.session_file_path,
|
||||||
|
'session_lock': self.session_lock,
|
||||||
|
'text_only_scope': self.text_only_scope,
|
||||||
|
'text_export_format': self.text_export_format,
|
||||||
|
'single_pdf_mode': self.single_pdf_mode,
|
||||||
|
'project_root_dir': self.project_root_dir,
|
||||||
|
}
|
||||||
|
# --- END OF FIX ---
|
||||||
|
|
||||||
|
post_processing_worker = PostProcessorWorker(**worker_args)
|
||||||
|
|
||||||
(dl_count, skip_count, kept_originals_this_post,
|
(dl_count, skip_count, kept_originals_this_post,
|
||||||
retryable_failures, permanent_failures,
|
retryable_failures, permanent_failures,
|
||||||
history_data, temp_filepath) = post_processing_worker.process()
|
history_data, temp_filepath) = post_processing_worker.process()
|
||||||
|
|
||||||
grand_total_downloaded_files += dl_count
|
grand_total_downloaded_files += dl_count
|
||||||
grand_total_skipped_files += skip_count
|
grand_total_skipped_files += skip_count
|
||||||
|
|
||||||
if kept_originals_this_post:
|
if kept_originals_this_post:
|
||||||
grand_list_of_kept_original_filenames.extend(kept_originals_this_post)
|
grand_list_of_kept_original_filenames.extend(kept_originals_this_post)
|
||||||
if retryable_failures:
|
if retryable_failures:
|
||||||
self.retryable_file_failed_signal.emit(retryable_failures)
|
self.retryable_file_failed_signal.emit(retryable_failures)
|
||||||
if history_data:
|
if history_data:
|
||||||
if len(self.history_candidates_buffer) < 8:
|
|
||||||
self.post_processed_for_history_signal.emit(history_data)
|
self.post_processed_for_history_signal.emit(history_data)
|
||||||
if permanent_failures:
|
if permanent_failures:
|
||||||
self.permanent_file_failed_signal.emit(permanent_failures)
|
self.permanent_file_failed_signal.emit(permanent_failures)
|
||||||
|
|
||||||
if self.single_pdf_mode and temp_filepath:
|
if self.single_pdf_mode and temp_filepath:
|
||||||
self.progress_signal.emit(f"TEMP_FILE_PATH:{temp_filepath}")
|
self.progress_signal.emit(f"TEMP_FILE_PATH:{temp_filepath}")
|
||||||
|
|
||||||
except Exception as proc_err:
|
|
||||||
post_id_for_err = individual_post_data.get('id', 'N/A')
|
|
||||||
self.logger(f"❌ Error processing post {post_id_for_err} in DownloadThread: {proc_err}")
|
|
||||||
traceback.print_exc()
|
|
||||||
num_potential_files_est = len(individual_post_data.get('attachments', [])) + (
|
|
||||||
1 if individual_post_data.get('file') else 0)
|
|
||||||
grand_total_skipped_files += num_potential_files_est
|
|
||||||
|
|
||||||
if self.skip_current_file_flag and self.skip_current_file_flag.is_set():
|
|
||||||
self.skip_current_file_flag.clear()
|
|
||||||
self.logger(" Skip current file flag was processed and cleared by DownloadThread.")
|
|
||||||
self.msleep(10)
|
|
||||||
if was_process_cancelled:
|
if was_process_cancelled:
|
||||||
break
|
break
|
||||||
|
|
||||||
if not was_process_cancelled and not self.isInterruptionRequested():
|
if not was_process_cancelled and not self.isInterruptionRequested():
|
||||||
self.logger("✅ All posts processed or end of content reached by DownloadThread.")
|
self.logger("✅ All posts processed or end of content reached by DownloadThread.")
|
||||||
|
|
||||||
@@ -1873,7 +1865,6 @@ class DownloadThread(QThread):
|
|||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
finally:
|
finally:
|
||||||
try:
|
try:
|
||||||
# Disconnect signals
|
|
||||||
if worker_signals_obj:
|
if worker_signals_obj:
|
||||||
worker_signals_obj.progress_signal.disconnect(self.progress_signal)
|
worker_signals_obj.progress_signal.disconnect(self.progress_signal)
|
||||||
worker_signals_obj.file_download_status_signal.disconnect(self.file_download_status_signal)
|
worker_signals_obj.file_download_status_signal.disconnect(self.file_download_status_signal)
|
||||||
@@ -1884,14 +1875,8 @@ class DownloadThread(QThread):
|
|||||||
except (TypeError, RuntimeError) as e:
|
except (TypeError, RuntimeError) as e:
|
||||||
self.logger(f"ℹ️ Note during DownloadThread signal disconnection: {e}")
|
self.logger(f"ℹ️ Note during DownloadThread signal disconnection: {e}")
|
||||||
|
|
||||||
# Emit the final signal with all collected results
|
|
||||||
self.finished_signal.emit(grand_total_downloaded_files, grand_total_skipped_files, self.isInterruptionRequested(), grand_list_of_kept_original_filenames)
|
self.finished_signal.emit(grand_total_downloaded_files, grand_total_skipped_files, self.isInterruptionRequested(), grand_list_of_kept_original_filenames)
|
||||||
|
|
||||||
def receive_add_character_result (self ,result ):
|
|
||||||
with QMutexLocker (self .prompt_mutex ):
|
|
||||||
self ._add_character_response =result
|
|
||||||
self .logger (f" (DownloadThread) Received character prompt response: {'Yes (added/confirmed)'if result else 'No (declined/failed)'}")
|
|
||||||
|
|
||||||
class InterruptedError(Exception):
|
class InterruptedError(Exception):
|
||||||
"""Custom exception for handling cancellations gracefully."""
|
"""Custom exception for handling cancellations gracefully."""
|
||||||
pass
|
pass
|
||||||
File diff suppressed because one or more lines are too long
@@ -9,9 +9,7 @@ from PyQt5.QtWidgets import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
# --- Local Application Imports ---
|
# --- Local Application Imports ---
|
||||||
# This assumes the new project structure is in place.
|
|
||||||
from ...i18n.translator import get_translation
|
from ...i18n.translator import get_translation
|
||||||
# get_app_icon_object is defined in the main window module in this refactoring plan.
|
|
||||||
from ..main_window import get_app_icon_object
|
from ..main_window import get_app_icon_object
|
||||||
from ...utils.resolution import get_dark_theme
|
from ...utils.resolution import get_dark_theme
|
||||||
|
|
||||||
@@ -42,21 +40,15 @@ class DownloadExtractedLinksDialog(QDialog):
|
|||||||
if not app_icon.isNull():
|
if not app_icon.isNull():
|
||||||
self.setWindowIcon(app_icon)
|
self.setWindowIcon(app_icon)
|
||||||
|
|
||||||
# Set window size dynamically based on the parent window's size
|
# --- START OF FIX ---
|
||||||
if parent:
|
# Get the user-defined scale factor from the parent application.
|
||||||
parent_width = parent.width()
|
scale_factor = getattr(self.parent_app, 'scale_factor', 1.0)
|
||||||
parent_height = parent.height()
|
|
||||||
# Use a scaling factor for different screen resolutions
|
|
||||||
screen_height = QApplication.primaryScreen().availableGeometry().height() if QApplication.primaryScreen() else 768
|
|
||||||
scale_factor = screen_height / 768.0
|
|
||||||
|
|
||||||
base_min_w, base_min_h = 500, 400
|
# Define base dimensions and apply the correct scale factor.
|
||||||
scaled_min_w = int(base_min_w * scale_factor)
|
base_width, base_height = 600, 450
|
||||||
scaled_min_h = int(base_min_h * scale_factor)
|
self.setMinimumSize(int(base_width * scale_factor), int(base_height * scale_factor))
|
||||||
|
self.resize(int(base_width * scale_factor * 1.1), int(base_height * scale_factor * 1.1))
|
||||||
self.setMinimumSize(scaled_min_w, scaled_min_h)
|
# --- END OF FIX ---
|
||||||
self.resize(max(int(parent_width * 0.6 * scale_factor), scaled_min_w),
|
|
||||||
max(int(parent_height * 0.7 * scale_factor), scaled_min_h))
|
|
||||||
|
|
||||||
# --- Initialize UI and Apply Theming ---
|
# --- Initialize UI and Apply Theming ---
|
||||||
self._init_ui()
|
self._init_ui()
|
||||||
|
|||||||
@@ -42,13 +42,11 @@ class ErrorFilesDialog(QDialog):
|
|||||||
if app_icon and not app_icon.isNull():
|
if app_icon and not app_icon.isNull():
|
||||||
self.setWindowIcon(app_icon)
|
self.setWindowIcon(app_icon)
|
||||||
|
|
||||||
# Set window size dynamically
|
scale_factor = getattr(self.parent_app, 'scale_factor', 1.0)
|
||||||
screen_height = QApplication.primaryScreen().availableGeometry().height() if QApplication.primaryScreen() else 768
|
|
||||||
scale_factor = screen_height / 1080.0
|
base_width, base_height = 550, 400
|
||||||
base_min_w, base_min_h = 500, 300
|
self.setMinimumSize(int(base_width * scale_factor), int(base_height * scale_factor))
|
||||||
scaled_min_w = int(base_min_w * scale_factor)
|
self.resize(int(base_width * scale_factor * 1.1), int(base_height * scale_factor * 1.1))
|
||||||
scaled_min_h = int(base_min_h * scale_factor)
|
|
||||||
self.setMinimumSize(scaled_min_w, scaled_min_h)
|
|
||||||
|
|
||||||
# --- Initialize UI and Apply Theming ---
|
# --- Initialize UI and Apply Theming ---
|
||||||
self._init_ui()
|
self._init_ui()
|
||||||
|
|||||||
@@ -20,15 +20,18 @@ class TourStepWidget(QWidget):
|
|||||||
A custom widget representing a single step or page in the feature guide.
|
A custom widget representing a single step or page in the feature guide.
|
||||||
It neatly formats a title and its corresponding content.
|
It neatly formats a title and its corresponding content.
|
||||||
"""
|
"""
|
||||||
def __init__(self, title_text, content_text, parent=None):
|
def __init__(self, title_text, content_text, parent=None, scale=1.0):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
layout = QVBoxLayout(self)
|
layout = QVBoxLayout(self)
|
||||||
layout.setContentsMargins(20, 20, 20, 20)
|
layout.setContentsMargins(20, 20, 20, 20)
|
||||||
layout.setSpacing(10)
|
layout.setSpacing(10)
|
||||||
|
|
||||||
|
title_font_size = int(14 * scale)
|
||||||
|
content_font_size = int(11 * scale)
|
||||||
|
|
||||||
title_label = QLabel(title_text)
|
title_label = QLabel(title_text)
|
||||||
title_label.setAlignment(Qt.AlignCenter)
|
title_label.setAlignment(Qt.AlignCenter)
|
||||||
title_label.setStyleSheet("font-size: 18px; font-weight: bold; color: #E0E0E0; padding-bottom: 15px;")
|
title_label.setStyleSheet(f"font-size: {title_font_size}pt; font-weight: bold; color: #E0E0E0; padding-bottom: 15px;")
|
||||||
layout.addWidget(title_label)
|
layout.addWidget(title_label)
|
||||||
|
|
||||||
scroll_area = QScrollArea()
|
scroll_area = QScrollArea()
|
||||||
@@ -42,8 +45,8 @@ class TourStepWidget(QWidget):
|
|||||||
content_label.setWordWrap(True)
|
content_label.setWordWrap(True)
|
||||||
content_label.setAlignment(Qt.AlignLeft | Qt.AlignTop)
|
content_label.setAlignment(Qt.AlignLeft | Qt.AlignTop)
|
||||||
content_label.setTextFormat(Qt.RichText)
|
content_label.setTextFormat(Qt.RichText)
|
||||||
content_label.setOpenExternalLinks(True) # Allow opening links in the content
|
content_label.setOpenExternalLinks(True)
|
||||||
content_label.setStyleSheet("font-size: 11pt; color: #C8C8C8; line-height: 1.8;")
|
content_label.setStyleSheet(f"font-size: {content_font_size}pt; color: #C8C8C8; line-height: 1.8;")
|
||||||
scroll_area.setWidget(content_label)
|
scroll_area.setWidget(content_label)
|
||||||
layout.addWidget(scroll_area, 1)
|
layout.addWidget(scroll_area, 1)
|
||||||
|
|
||||||
@@ -56,27 +59,38 @@ class HelpGuideDialog (QDialog ):
|
|||||||
self .steps_data =steps_data
|
self .steps_data =steps_data
|
||||||
self .parent_app =parent_app
|
self .parent_app =parent_app
|
||||||
|
|
||||||
|
scale = self.parent_app.scale_factor if hasattr(self.parent_app, 'scale_factor') else 1.0
|
||||||
|
|
||||||
app_icon = get_app_icon_object()
|
app_icon = get_app_icon_object()
|
||||||
if app_icon and not app_icon.isNull():
|
if app_icon and not app_icon.isNull():
|
||||||
self.setWindowIcon(app_icon)
|
self.setWindowIcon(app_icon)
|
||||||
|
|
||||||
self.setModal(True)
|
self.setModal(True)
|
||||||
self .setFixedSize (650 ,600 )
|
self.resize(int(650 * scale), int(600 * scale))
|
||||||
|
|
||||||
|
dialog_font_size = int(11 * scale)
|
||||||
|
|
||||||
current_theme_style = ""
|
current_theme_style = ""
|
||||||
if hasattr(self.parent_app, 'current_theme') and self.parent_app.current_theme == "dark":
|
if hasattr(self.parent_app, 'current_theme') and self.parent_app.current_theme == "dark":
|
||||||
if hasattr (self .parent_app ,'get_dark_theme'):
|
current_theme_style = get_dark_theme(scale)
|
||||||
current_theme_style =self .parent_app .get_dark_theme ()
|
else:
|
||||||
|
current_theme_style = f"""
|
||||||
|
QDialog {{ background-color: #F0F0F0; border: 1px solid #B0B0B0; }}
|
||||||
|
QLabel {{ color: #1E1E1E; }}
|
||||||
|
QPushButton {{
|
||||||
|
background-color: #E1E1E1;
|
||||||
|
color: #1E1E1E;
|
||||||
|
border: 1px solid #ADADAD;
|
||||||
|
padding: {int(8*scale)}px {int(15*scale)}px;
|
||||||
|
border-radius: 4px;
|
||||||
|
min-height: {int(25*scale)}px;
|
||||||
|
font-size: {dialog_font_size}pt;
|
||||||
|
}}
|
||||||
|
QPushButton:hover {{ background-color: #CACACA; }}
|
||||||
|
QPushButton:pressed {{ background-color: #B0B0B0; }}
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.setStyleSheet(current_theme_style)
|
||||||
self .setStyleSheet (current_theme_style if current_theme_style else """
|
|
||||||
QDialog { background-color: #2E2E2E; border: 1px solid #5A5A5A; }
|
|
||||||
QLabel { color: #E0E0E0; }
|
|
||||||
QPushButton { background-color: #555; color: #F0F0F0; border: 1px solid #6A6A6A; padding: 8px 15px; border-radius: 4px; min-height: 25px; font-size: 11pt; }
|
|
||||||
QPushButton:hover { background-color: #656565; }
|
|
||||||
QPushButton:pressed { background-color: #4A4A4A; }
|
|
||||||
""")
|
|
||||||
self ._init_ui ()
|
self ._init_ui ()
|
||||||
if self .parent_app :
|
if self .parent_app :
|
||||||
self .move (self .parent_app .geometry ().center ()-self .rect ().center ())
|
self .move (self .parent_app .geometry ().center ()-self .rect ().center ())
|
||||||
@@ -97,8 +111,9 @@ class HelpGuideDialog (QDialog ):
|
|||||||
main_layout .addWidget (self .stacked_widget ,1 )
|
main_layout .addWidget (self .stacked_widget ,1 )
|
||||||
|
|
||||||
self .tour_steps_widgets =[]
|
self .tour_steps_widgets =[]
|
||||||
|
scale = self.parent_app.scale_factor if hasattr(self.parent_app, 'scale_factor') else 1.0
|
||||||
for title, content in self.steps_data:
|
for title, content in self.steps_data:
|
||||||
step_widget =TourStepWidget (title ,content )
|
step_widget = TourStepWidget(title, content, scale=scale)
|
||||||
self.tour_steps_widgets.append(step_widget)
|
self.tour_steps_widgets.append(step_widget)
|
||||||
self.stacked_widget.addWidget(step_widget)
|
self.stacked_widget.addWidget(step_widget)
|
||||||
|
|
||||||
@@ -115,7 +130,6 @@ class HelpGuideDialog (QDialog ):
|
|||||||
if getattr (sys ,'frozen',False )and hasattr (sys ,'_MEIPASS'):
|
if getattr (sys ,'frozen',False )and hasattr (sys ,'_MEIPASS'):
|
||||||
assets_base_dir =sys ._MEIPASS
|
assets_base_dir =sys ._MEIPASS
|
||||||
else :
|
else :
|
||||||
# Go up three levels from this file's directory (src/ui/dialogs) to the project root
|
|
||||||
assets_base_dir =os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..'))
|
assets_base_dir =os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..'))
|
||||||
|
|
||||||
github_icon_path =os .path .join (assets_base_dir ,"assets","github.png")
|
github_icon_path =os .path .join (assets_base_dir ,"assets","github.png")
|
||||||
@@ -126,7 +140,9 @@ class HelpGuideDialog (QDialog ):
|
|||||||
self .instagram_button =QPushButton (QIcon (instagram_icon_path ),"")
|
self .instagram_button =QPushButton (QIcon (instagram_icon_path ),"")
|
||||||
self .Discord_button =QPushButton (QIcon (discord_icon_path ),"")
|
self .Discord_button =QPushButton (QIcon (discord_icon_path ),"")
|
||||||
|
|
||||||
icon_size =QSize (24 ,24 )
|
scale = self.parent_app.scale_factor if hasattr(self.parent_app, 'scale_factor') else 1.0
|
||||||
|
icon_dim = int(24 * scale)
|
||||||
|
icon_size = QSize(icon_dim, icon_dim)
|
||||||
self .github_button .setIconSize (icon_size )
|
self .github_button .setIconSize (icon_size )
|
||||||
self .instagram_button .setIconSize (icon_size )
|
self .instagram_button .setIconSize (icon_size )
|
||||||
self .Discord_button .setIconSize (icon_size )
|
self .Discord_button .setIconSize (icon_size )
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ class KnownNamesFilterDialog(QDialog):
|
|||||||
"""
|
"""
|
||||||
A dialog to select names from the Known.txt list to add to the main
|
A dialog to select names from the Known.txt list to add to the main
|
||||||
character filter input field. This provides a convenient way for users
|
character filter input field. This provides a convenient way for users
|
||||||
|
|
||||||
to reuse their saved names and groups for filtering downloads.
|
to reuse their saved names and groups for filtering downloads.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -40,11 +39,10 @@ class KnownNamesFilterDialog(QDialog):
|
|||||||
|
|
||||||
# Set window size dynamically
|
# Set window size dynamically
|
||||||
screen_geometry = QApplication.primaryScreen().availableGeometry()
|
screen_geometry = QApplication.primaryScreen().availableGeometry()
|
||||||
|
scale_factor = getattr(self.parent_app, 'scale_factor', 1.0)
|
||||||
base_width, base_height = 460, 450
|
base_width, base_height = 460, 450
|
||||||
scale_factor_h = screen_geometry.height() / 1080.0
|
self.setMinimumSize(int(base_width * scale_factor), int(base_height * scale_factor))
|
||||||
effective_scale_factor = max(0.75, min(scale_factor_h, 1.5))
|
self.resize(int(base_width * scale_factor * 1.1), int(base_height * scale_factor * 1.1))
|
||||||
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))
|
|
||||||
|
|
||||||
# --- Initialize UI and Apply Theming ---
|
# --- Initialize UI and Apply Theming ---
|
||||||
self._init_ui()
|
self._init_ui()
|
||||||
|
|||||||
@@ -260,7 +260,7 @@ class DownloaderApp (QWidget ):
|
|||||||
self.download_location_label_widget = None
|
self.download_location_label_widget = None
|
||||||
self.remove_from_filename_label_widget = None
|
self.remove_from_filename_label_widget = None
|
||||||
self.skip_words_label_widget = None
|
self.skip_words_label_widget = None
|
||||||
self.setWindowTitle("Kemono Downloader v6.0.0")
|
self.setWindowTitle("Kemono Downloader v6.1.0")
|
||||||
setup_ui(self)
|
setup_ui(self)
|
||||||
self._connect_signals()
|
self._connect_signals()
|
||||||
self.log_signal.emit("ℹ️ Local API server functionality has been removed.")
|
self.log_signal.emit("ℹ️ Local API server functionality has been removed.")
|
||||||
@@ -303,7 +303,7 @@ class DownloaderApp (QWidget ):
|
|||||||
if msg_box.clickedButton() == restart_button:
|
if msg_box.clickedButton() == restart_button:
|
||||||
self._request_restart_application()
|
self._request_restart_application()
|
||||||
|
|
||||||
def _create_initial_session_file(self, api_url_for_session, override_output_dir_for_session): # ADD override_output_dir_for_session
|
def _create_initial_session_file(self, api_url_for_session, override_output_dir_for_session, remaining_queue=None):
|
||||||
"""Creates the initial session file at the start of a new download."""
|
"""Creates the initial session file at the start of a new download."""
|
||||||
if self.is_restore_pending:
|
if self.is_restore_pending:
|
||||||
return
|
return
|
||||||
@@ -319,11 +319,14 @@ class DownloaderApp (QWidget ):
|
|||||||
"download_state": {
|
"download_state": {
|
||||||
"processed_post_ids": [],
|
"processed_post_ids": [],
|
||||||
"permanently_failed_files": [],
|
"permanently_failed_files": [],
|
||||||
|
"successfully_downloaded_hashes": [],
|
||||||
|
"last_processed_offset": 0,
|
||||||
"manga_counters": {
|
"manga_counters": {
|
||||||
"date_based": 1,
|
"date_based": 1,
|
||||||
"global_numbering": 1
|
"global_numbering": 1
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"remaining_queue": list(remaining_queue) if remaining_queue else []
|
||||||
}
|
}
|
||||||
self._save_session_file(session_data)
|
self._save_session_file(session_data)
|
||||||
|
|
||||||
@@ -331,7 +334,6 @@ class DownloaderApp (QWidget ):
|
|||||||
"""Returns a mapping of checkbox attribute names to their corresponding settings key."""
|
"""Returns a mapping of checkbox attribute names to their corresponding settings key."""
|
||||||
return {
|
return {
|
||||||
'skip_zip_checkbox': 'skip_zip',
|
'skip_zip_checkbox': 'skip_zip',
|
||||||
'skip_rar_checkbox': 'skip_rar',
|
|
||||||
'download_thumbnails_checkbox': 'download_thumbnails',
|
'download_thumbnails_checkbox': 'download_thumbnails',
|
||||||
'compress_images_checkbox': 'compress_images',
|
'compress_images_checkbox': 'compress_images',
|
||||||
'use_subfolders_checkbox': 'use_subfolders',
|
'use_subfolders_checkbox': 'use_subfolders',
|
||||||
@@ -377,6 +379,11 @@ class DownloaderApp (QWidget ):
|
|||||||
settings['char_filter_scope'] = self.char_filter_scope
|
settings['char_filter_scope'] = self.char_filter_scope
|
||||||
settings['manga_filename_style'] = self.manga_filename_style
|
settings['manga_filename_style'] = self.manga_filename_style
|
||||||
settings['allow_multipart_download'] = self.allow_multipart_download_setting
|
settings['allow_multipart_download'] = self.allow_multipart_download_setting
|
||||||
|
settings['more_filter_scope'] = self.more_filter_scope
|
||||||
|
settings['text_export_format'] = self.text_export_format
|
||||||
|
settings['single_pdf_setting'] = self.single_pdf_setting
|
||||||
|
settings['keep_duplicates_mode'] = self.keep_duplicates_mode
|
||||||
|
settings['keep_duplicates_limit'] = self.keep_duplicates_limit
|
||||||
|
|
||||||
return settings
|
return settings
|
||||||
|
|
||||||
@@ -414,6 +421,12 @@ class DownloaderApp (QWidget ):
|
|||||||
self.permanently_failed_files_for_dialog.extend(failed_files_from_session)
|
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.log_signal.emit(f"ℹ️ Restored {len(failed_files_from_session)} failed file entries from the previous session.")
|
||||||
|
|
||||||
|
remaining_queue_from_session = session_data.get('remaining_queue', [])
|
||||||
|
if remaining_queue_from_session:
|
||||||
|
self.favorite_download_queue.clear()
|
||||||
|
self.favorite_download_queue.extend(remaining_queue_from_session)
|
||||||
|
self.log_signal.emit(f"ℹ️ Restored {len(self.favorite_download_queue)} creator(s) to the download queue.")
|
||||||
|
|
||||||
self.interrupted_session_data = session_data
|
self.interrupted_session_data = session_data
|
||||||
self.log_signal.emit("ℹ️ Incomplete download session found. UI updated for restore.")
|
self.log_signal.emit("ℹ️ Incomplete download session found. UI updated for restore.")
|
||||||
self._prepare_ui_for_restore()
|
self._prepare_ui_for_restore()
|
||||||
@@ -457,6 +470,9 @@ class DownloaderApp (QWidget ):
|
|||||||
"""Safely saves the session data to the session file using an atomic write pattern."""
|
"""Safely saves the session data to the session file using an atomic write pattern."""
|
||||||
temp_session_file_path = self.session_file_path + ".tmp"
|
temp_session_file_path = self.session_file_path + ".tmp"
|
||||||
try:
|
try:
|
||||||
|
if 'download_state' in session_data:
|
||||||
|
with self.downloaded_file_hashes_lock:
|
||||||
|
session_data['download_state']['successfully_downloaded_hashes'] = list(self.downloaded_file_hashes)
|
||||||
with open(temp_session_file_path, 'w', encoding='utf-8') as f:
|
with open(temp_session_file_path, 'w', encoding='utf-8') as f:
|
||||||
json.dump(session_data, f, indent=2)
|
json.dump(session_data, f, indent=2)
|
||||||
os.replace(temp_session_file_path, self.session_file_path)
|
os.replace(temp_session_file_path, self.session_file_path)
|
||||||
@@ -554,11 +570,10 @@ class DownloaderApp (QWidget ):
|
|||||||
self ._update_skip_scope_button_text ()
|
self ._update_skip_scope_button_text ()
|
||||||
|
|
||||||
if hasattr (self ,'skip_zip_checkbox'):self .skip_zip_checkbox .setText (self ._tr ("skip_zip_checkbox_label","Skip .zip"))
|
if hasattr (self ,'skip_zip_checkbox'):self .skip_zip_checkbox .setText (self ._tr ("skip_zip_checkbox_label","Skip .zip"))
|
||||||
if hasattr (self ,'skip_rar_checkbox'):self .skip_rar_checkbox .setText (self ._tr ("skip_rar_checkbox_label","Skip .rar"))
|
|
||||||
if hasattr (self ,'download_thumbnails_checkbox'):self .download_thumbnails_checkbox .setText (self ._tr ("download_thumbnails_checkbox_label","Download Thumbnails Only"))
|
if hasattr (self ,'download_thumbnails_checkbox'):self .download_thumbnails_checkbox .setText (self ._tr ("download_thumbnails_checkbox_label","Download Thumbnails Only"))
|
||||||
if hasattr (self ,'scan_content_images_checkbox'):self .scan_content_images_checkbox .setText (self ._tr ("scan_content_images_checkbox_label","Scan Content for Images"))
|
if hasattr (self ,'scan_content_images_checkbox'):self .scan_content_images_checkbox .setText (self ._tr ("scan_content_images_checkbox_label","Scan Content for Images"))
|
||||||
if hasattr (self ,'compress_images_checkbox'):self .compress_images_checkbox .setText (self ._tr ("compress_images_checkbox_label","Compress to WebP"))
|
if hasattr (self ,'compress_images_checkbox'):self .compress_images_checkbox .setText (self ._tr ("compress_images_checkbox_label","Compress to WebP"))
|
||||||
if hasattr (self ,'use_subfolders_checkbox'):self .use_subfolders_checkbox .setText (self ._tr ("separate_folders_checkbox_label","Separate Folders by Name/Title"))
|
if hasattr (self ,'use_subfolders_checkbox'):self .use_subfolders_checkbox .setText (self ._tr ("separate_folders_checkbox_label","Separate Folders by Known.txt"))
|
||||||
if hasattr (self ,'use_subfolder_per_post_checkbox'):self .use_subfolder_per_post_checkbox .setText (self ._tr ("subfolder_per_post_checkbox_label","Subfolder per Post"))
|
if hasattr (self ,'use_subfolder_per_post_checkbox'):self .use_subfolder_per_post_checkbox .setText (self ._tr ("subfolder_per_post_checkbox_label","Subfolder per Post"))
|
||||||
if hasattr (self ,'use_cookie_checkbox'):self .use_cookie_checkbox .setText (self ._tr ("use_cookie_checkbox_label","Use Cookie"))
|
if hasattr (self ,'use_cookie_checkbox'):self .use_cookie_checkbox .setText (self ._tr ("use_cookie_checkbox_label","Use Cookie"))
|
||||||
if hasattr (self ,'use_multithreading_checkbox'):self .update_multithreading_label (self .thread_count_input .text ()if hasattr (self ,'thread_count_input')else "1")
|
if hasattr (self ,'use_multithreading_checkbox'):self .update_multithreading_label (self .thread_count_input .text ()if hasattr (self ,'thread_count_input')else "1")
|
||||||
@@ -931,28 +946,20 @@ class DownloaderApp (QWidget ):
|
|||||||
"""
|
"""
|
||||||
global KNOWN_NAMES
|
global KNOWN_NAMES
|
||||||
try:
|
try:
|
||||||
# --- FIX STARTS HERE ---
|
|
||||||
# Get the directory path from the full file path.
|
|
||||||
config_dir = os.path.dirname(self.config_file)
|
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)
|
os.makedirs(config_dir, exist_ok=True)
|
||||||
# --- FIX ENDS HERE ---
|
|
||||||
|
|
||||||
with open(self.config_file, 'w', encoding='utf-8') as f:
|
with open(self.config_file, 'w', encoding='utf-8') as f:
|
||||||
for entry in KNOWN_NAMES:
|
for entry in KNOWN_NAMES:
|
||||||
if entry["is_group"]:
|
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")
|
f.write(f"({', '.join(sorted(entry['aliases'], key=str.lower))})\n")
|
||||||
else:
|
else:
|
||||||
# For single entries, write the name on its own line.
|
|
||||||
f.write(entry["name"] + '\n')
|
f.write(entry["name"] + '\n')
|
||||||
|
|
||||||
if hasattr(self, 'log_signal'):
|
if hasattr(self, 'log_signal'):
|
||||||
self.log_signal.emit(f"💾 Saved {len(KNOWN_NAMES)} known entries to {self.config_file}")
|
self.log_signal.emit(f"💾 Saved {len(KNOWN_NAMES)} known entries to {self.config_file}")
|
||||||
|
|
||||||
except Exception as e:
|
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}"
|
log_msg = f"❌ Error saving config '{self.config_file}': {e}"
|
||||||
if hasattr(self, 'log_signal'):
|
if hasattr(self, 'log_signal'):
|
||||||
self.log_signal.emit(log_msg)
|
self.log_signal.emit(log_msg)
|
||||||
@@ -1489,32 +1496,32 @@ class DownloaderApp (QWidget ):
|
|||||||
QMessageBox .critical (self ,"Dialog Error",f"An unexpected error occurred with the folder selection dialog: {e }")
|
QMessageBox .critical (self ,"Dialog Error",f"An unexpected error occurred with the folder selection dialog: {e }")
|
||||||
|
|
||||||
def handle_main_log(self, message):
|
def handle_main_log(self, message):
|
||||||
# vvv ADD THIS BLOCK AT THE TOP OF THE METHOD vvv
|
|
||||||
if message.startswith("TEMP_FILE_PATH:"):
|
if message.startswith("TEMP_FILE_PATH:"):
|
||||||
filepath = message.split(":", 1)[1]
|
filepath = message.split(":", 1)[1]
|
||||||
if self.single_pdf_setting:
|
if self.single_pdf_setting:
|
||||||
self.session_temp_files.append(filepath)
|
self.session_temp_files.append(filepath)
|
||||||
return
|
return
|
||||||
is_html_message =message .startswith (HTML_PREFIX )
|
|
||||||
display_message =message
|
|
||||||
use_html =False
|
|
||||||
|
|
||||||
if is_html_message :
|
is_html_message = message.startswith(HTML_PREFIX)
|
||||||
display_message =message [len (HTML_PREFIX ):]
|
display_message = message[len(HTML_PREFIX):] if is_html_message else message
|
||||||
use_html =True
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
safe_message = str(display_message).replace('\x00', '[NULL]')
|
safe_message = str(display_message).replace('\x00', '[NULL]')
|
||||||
if use_html :
|
lines = safe_message.split('\n')
|
||||||
self .main_log_output .insertHtml (safe_message )
|
|
||||||
|
for line in lines:
|
||||||
|
if is_html_message:
|
||||||
|
self.main_log_output.insertHtml(line + "<br>")
|
||||||
else:
|
else:
|
||||||
self .main_log_output .append (safe_message )
|
self.main_log_output.append(line)
|
||||||
|
|
||||||
scrollbar = self.main_log_output.verticalScrollBar()
|
scrollbar = self.main_log_output.verticalScrollBar()
|
||||||
if scrollbar.value() >= scrollbar.maximum() - 30:
|
if scrollbar.value() >= scrollbar.maximum() - 30:
|
||||||
scrollbar.setValue(scrollbar.maximum())
|
scrollbar.setValue(scrollbar.maximum())
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"GUI Main Log Error: {e}\nOriginal Message: {message}")
|
print(f"GUI Main Log Error: {e}\nOriginal Message: {message}")
|
||||||
|
|
||||||
def _extract_key_term_from_title (self ,title ):
|
def _extract_key_term_from_title (self ,title ):
|
||||||
if not title :
|
if not title :
|
||||||
return None
|
return None
|
||||||
@@ -1635,26 +1642,48 @@ class DownloaderApp (QWidget ):
|
|||||||
post_title ,link_text ,link_url ,platform ,decryption_key =link_data
|
post_title ,link_text ,link_url ,platform ,decryption_key =link_data
|
||||||
is_only_links_mode =self .radio_only_links and self .radio_only_links .isChecked ()
|
is_only_links_mode =self .radio_only_links and self .radio_only_links .isChecked ()
|
||||||
|
|
||||||
max_link_text_len =50
|
|
||||||
display_text =(link_text [:max_link_text_len ].strip ()+"..."
|
|
||||||
if len (link_text )>max_link_text_len else link_text .strip ())
|
|
||||||
formatted_link_info =f"{display_text } - {link_url } - {platform }"
|
|
||||||
|
|
||||||
if decryption_key :
|
|
||||||
formatted_link_info +=f" (Decryption Key: {decryption_key })"
|
|
||||||
|
|
||||||
if is_only_links_mode:
|
if is_only_links_mode:
|
||||||
|
# Check if this is a new post title
|
||||||
if post_title != self._current_link_post_title:
|
if post_title != self._current_link_post_title:
|
||||||
separator_html ="<br>"+"-"*45 +"<br>"
|
# Add a styled horizontal rule as a separator
|
||||||
if self._current_link_post_title is not None:
|
if self._current_link_post_title is not None:
|
||||||
self .log_signal .emit (HTML_PREFIX +separator_html )
|
separator_html = f'{HTML_PREFIX}<hr style="border: 1px solid #444;">'
|
||||||
title_html =f'<b style="color: #87CEEB;">{html .escape (post_title )}</b><br>'
|
self.log_signal.emit(separator_html)
|
||||||
self .log_signal .emit (HTML_PREFIX +title_html )
|
|
||||||
|
# Display the new post title as a styled heading
|
||||||
|
title_html = f'{HTML_PREFIX}<h3 style="color: #87CEEB; margin-bottom: 5px; margin-top: 8px;">{html.escape(post_title)}</h3>'
|
||||||
|
self.log_signal.emit(title_html)
|
||||||
self._current_link_post_title = post_title
|
self._current_link_post_title = post_title
|
||||||
|
|
||||||
self .log_signal .emit (formatted_link_info )
|
# Sanitize the link text for safe HTML display
|
||||||
|
display_text = html.escape(link_text.strip() if link_text.strip() else link_url)
|
||||||
|
|
||||||
|
# Build the HTML for the link item for a cleaner look
|
||||||
|
link_html_parts = [
|
||||||
|
# Use a div for indentation and a bullet point for list-like appearance
|
||||||
|
f'<div style="margin-left: 20px; margin-bottom: 4px;">'
|
||||||
|
f'• <a href="{link_url}" style="color: #A9D0F5; text-decoration: none;">{display_text}</a>'
|
||||||
|
f' <span style="color: #999;">({html.escape(platform)})</span>'
|
||||||
|
]
|
||||||
|
|
||||||
|
if decryption_key:
|
||||||
|
link_html_parts.append(
|
||||||
|
# Display key on a new line, indented, and in a different color
|
||||||
|
f'<br><span style="margin-left: 15px; color: #f0ad4e; font-size: 9pt;">'
|
||||||
|
f'Key: {html.escape(decryption_key)}</span>'
|
||||||
|
)
|
||||||
|
|
||||||
|
link_html_parts.append('</div>')
|
||||||
|
|
||||||
|
final_link_html = f'{HTML_PREFIX}{"".join(link_html_parts)}'
|
||||||
|
self.log_signal.emit(final_link_html)
|
||||||
|
|
||||||
|
# This part handles the secondary log panel and remains the same
|
||||||
elif self .show_external_links :
|
elif self .show_external_links :
|
||||||
separator ="-"*45
|
separator ="-"*45
|
||||||
|
formatted_link_info = f"{link_text} - {link_url} - {platform}"
|
||||||
|
if decryption_key:
|
||||||
|
formatted_link_info += f" (Decryption Key: {decryption_key})"
|
||||||
self._append_to_external_log(formatted_link_info, separator)
|
self._append_to_external_log(formatted_link_info, separator)
|
||||||
|
|
||||||
self ._is_processing_external_link_queue =False
|
self ._is_processing_external_link_queue =False
|
||||||
@@ -1787,19 +1816,31 @@ class DownloaderApp (QWidget ):
|
|||||||
self.log_signal.emit("ℹ️ External Links Log Disabled")
|
self.log_signal.emit("ℹ️ External Links Log Disabled")
|
||||||
|
|
||||||
def _handle_filter_mode_change(self, button, checked):
|
def _handle_filter_mode_change(self, button, checked):
|
||||||
# If a button other than "More" is selected, reset the UI
|
|
||||||
if button != self.radio_more and checked:
|
|
||||||
self.radio_more.setText("More")
|
|
||||||
self.more_filter_scope = None
|
|
||||||
self.single_pdf_setting = False # Reset the setting
|
|
||||||
# Re-enable the checkboxes
|
|
||||||
if hasattr(self, 'use_multithreading_checkbox'): self.use_multithreading_checkbox.setEnabled(True)
|
|
||||||
if hasattr(self, 'use_subfolders_checkbox'): self.use_subfolders_checkbox.setEnabled(True)
|
|
||||||
|
|
||||||
if not button or not checked:
|
if not button or not checked:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Define this variable early to ensure it's always available.
|
||||||
is_only_links = (button == self.radio_only_links)
|
is_only_links = (button == self.radio_only_links)
|
||||||
|
|
||||||
|
# Handle the automatic disabling of multithreading for link extraction
|
||||||
|
if hasattr(self, 'use_multithreading_checkbox'):
|
||||||
|
if is_only_links:
|
||||||
|
# Disable multithreading for "Only Links" to avoid the bug
|
||||||
|
self.use_multithreading_checkbox.setChecked(False)
|
||||||
|
self.use_multithreading_checkbox.setEnabled(False)
|
||||||
|
else:
|
||||||
|
# Re-enable the multithreading option for other modes.
|
||||||
|
# Other logic will handle disabling it if needed (e.g., for Manga Date mode).
|
||||||
|
self.use_multithreading_checkbox.setEnabled(True)
|
||||||
|
|
||||||
|
# Reset the "More" button text if another button is selected
|
||||||
|
if button != self.radio_more and checked:
|
||||||
|
self.radio_more.setText("More")
|
||||||
|
self.more_filter_scope = None
|
||||||
|
self.single_pdf_setting = False
|
||||||
|
if hasattr(self, 'use_subfolders_checkbox'):
|
||||||
|
self.use_subfolders_checkbox.setEnabled(True)
|
||||||
|
|
||||||
is_only_audio = (hasattr(self, 'radio_only_audio') and self.radio_only_audio is not None and button == self.radio_only_audio)
|
is_only_audio = (hasattr(self, 'radio_only_audio') and self.radio_only_audio is not None and button == self.radio_only_audio)
|
||||||
is_only_archives = (hasattr(self, 'radio_only_archives') and self.radio_only_archives is not None and button == self.radio_only_archives)
|
is_only_archives = (hasattr(self, 'radio_only_archives') and self.radio_only_archives is not None and button == self.radio_only_archives)
|
||||||
|
|
||||||
@@ -1827,8 +1868,6 @@ class DownloaderApp (QWidget ):
|
|||||||
|
|
||||||
file_download_mode_active = not is_only_links
|
file_download_mode_active = not is_only_links
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if self.use_subfolders_checkbox: self.use_subfolders_checkbox.setEnabled(file_download_mode_active)
|
if self.use_subfolders_checkbox: self.use_subfolders_checkbox.setEnabled(file_download_mode_active)
|
||||||
if self.skip_words_input: self.skip_words_input.setEnabled(file_download_mode_active)
|
if self.skip_words_input: self.skip_words_input.setEnabled(file_download_mode_active)
|
||||||
if self.skip_scope_toggle_button: self.skip_scope_toggle_button.setEnabled(file_download_mode_active)
|
if self.skip_scope_toggle_button: self.skip_scope_toggle_button.setEnabled(file_download_mode_active)
|
||||||
@@ -1840,12 +1879,6 @@ class DownloaderApp (QWidget ):
|
|||||||
if is_only_archives:
|
if is_only_archives:
|
||||||
self.skip_zip_checkbox.setChecked(False)
|
self.skip_zip_checkbox.setChecked(False)
|
||||||
|
|
||||||
if self .skip_rar_checkbox :
|
|
||||||
can_skip_rar =file_download_mode_active and not is_only_archives
|
|
||||||
self .skip_rar_checkbox .setEnabled (can_skip_rar )
|
|
||||||
if is_only_archives :
|
|
||||||
self .skip_rar_checkbox .setChecked (False )
|
|
||||||
|
|
||||||
other_file_proc_enabled = file_download_mode_active and not is_only_archives
|
other_file_proc_enabled = file_download_mode_active and not is_only_archives
|
||||||
if self.download_thumbnails_checkbox: self.download_thumbnails_checkbox.setEnabled(other_file_proc_enabled)
|
if self.download_thumbnails_checkbox: self.download_thumbnails_checkbox.setEnabled(other_file_proc_enabled)
|
||||||
if self.compress_images_checkbox: self.compress_images_checkbox.setEnabled(other_file_proc_enabled)
|
if self.compress_images_checkbox: self.compress_images_checkbox.setEnabled(other_file_proc_enabled)
|
||||||
@@ -1856,13 +1889,11 @@ class DownloaderApp (QWidget ):
|
|||||||
if not can_show_external_log_option:
|
if not can_show_external_log_option:
|
||||||
self.external_links_checkbox.setChecked(False)
|
self.external_links_checkbox.setChecked(False)
|
||||||
|
|
||||||
|
|
||||||
if is_only_links:
|
if is_only_links:
|
||||||
self.progress_log_label.setText("📜 Extracted Links Log:")
|
self.progress_log_label.setText("📜 Extracted Links Log:")
|
||||||
if self.external_log_output: self.external_log_output.hide()
|
if self.external_log_output: self.external_log_output.hide()
|
||||||
if self.log_splitter: self.log_splitter.setSizes([self.height(), 0])
|
if self.log_splitter: self.log_splitter.setSizes([self.height(), 0])
|
||||||
|
|
||||||
|
|
||||||
do_clear_log_in_filter_change = True
|
do_clear_log_in_filter_change = True
|
||||||
if self.mega_download_log_preserved_once and self.only_links_log_display_mode == LOG_DISPLAY_DOWNLOAD_PROGRESS:
|
if self.mega_download_log_preserved_once and self.only_links_log_display_mode == LOG_DISPLAY_DOWNLOAD_PROGRESS:
|
||||||
do_clear_log_in_filter_change = False
|
do_clear_log_in_filter_change = False
|
||||||
@@ -1892,7 +1923,6 @@ class DownloaderApp (QWidget ):
|
|||||||
self.update_external_links_setting(self.external_links_checkbox.isChecked() if self.external_links_checkbox else False)
|
self.update_external_links_setting(self.external_links_checkbox.isChecked() if self.external_links_checkbox else False)
|
||||||
self.log_signal.emit(f"ℹ️ Filter mode changed to: {button.text()}")
|
self.log_signal.emit(f"ℹ️ Filter mode changed to: {button.text()}")
|
||||||
|
|
||||||
|
|
||||||
if is_only_links:
|
if is_only_links:
|
||||||
self._filter_links_log()
|
self._filter_links_log()
|
||||||
|
|
||||||
@@ -2634,24 +2664,39 @@ class DownloaderApp (QWidget ):
|
|||||||
if total_posts >0 or processed_posts >0 :
|
if total_posts >0 or processed_posts >0 :
|
||||||
self .file_progress_label .setText ("")
|
self .file_progress_label .setText ("")
|
||||||
|
|
||||||
def start_download(self, direct_api_url=None, override_output_dir=None, is_restore=False):
|
def start_download(self, direct_api_url=None, override_output_dir=None, is_restore=False, is_continuation=False):
|
||||||
self.is_finishing = False
|
self.is_finishing = False
|
||||||
self.downloaded_hash_counts.clear()
|
self.downloaded_hash_counts.clear()
|
||||||
global KNOWN_NAMES, BackendDownloadThread, PostProcessorWorker, extract_post_info, clean_folder_name, MAX_FILE_THREADS_PER_POST_OR_WORKER
|
global KNOWN_NAMES, BackendDownloadThread, PostProcessorWorker, extract_post_info, clean_folder_name, MAX_FILE_THREADS_PER_POST_OR_WORKER
|
||||||
|
|
||||||
|
if not is_restore and not is_continuation:
|
||||||
|
self.permanently_failed_files_for_dialog.clear()
|
||||||
|
|
||||||
|
self.retryable_failed_files_info.clear()
|
||||||
|
self._update_error_button_count()
|
||||||
|
|
||||||
self._clear_stale_temp_files()
|
self._clear_stale_temp_files()
|
||||||
self.session_temp_files = []
|
self.session_temp_files = []
|
||||||
|
|
||||||
processed_post_ids_for_restore = []
|
processed_post_ids_for_restore = []
|
||||||
manga_counters_for_restore = None
|
manga_counters_for_restore = None
|
||||||
|
start_offset_for_restore = 0
|
||||||
|
|
||||||
if is_restore and self.interrupted_session_data:
|
if is_restore and self.interrupted_session_data:
|
||||||
self.log_signal.emit(" Restoring session state...")
|
self.log_signal.emit(" Restoring session state...")
|
||||||
download_state = self.interrupted_session_data.get("download_state", {})
|
download_state = self.interrupted_session_data.get("download_state", {})
|
||||||
processed_post_ids_for_restore = download_state.get("processed_post_ids", [])
|
processed_post_ids_for_restore = download_state.get("processed_post_ids", [])
|
||||||
|
start_offset_for_restore = download_state.get("last_processed_offset", 0)
|
||||||
|
restored_hashes = download_state.get("successfully_downloaded_hashes", [])
|
||||||
|
if restored_hashes:
|
||||||
|
with self.downloaded_file_hashes_lock:
|
||||||
|
self.downloaded_file_hashes.update(restored_hashes)
|
||||||
|
self.log_signal.emit(f" Restored memory of {len(restored_hashes)} successfully downloaded files.")
|
||||||
manga_counters_for_restore = download_state.get("manga_counters")
|
manga_counters_for_restore = download_state.get("manga_counters")
|
||||||
if processed_post_ids_for_restore:
|
if processed_post_ids_for_restore:
|
||||||
self.log_signal.emit(f" Will skip {len(processed_post_ids_for_restore)} already processed posts.")
|
self.log_signal.emit(f" Will skip {len(processed_post_ids_for_restore)} already processed posts.")
|
||||||
|
if start_offset_for_restore > 0:
|
||||||
|
self.log_signal.emit(f" Resuming fetch from page offset: {start_offset_for_restore}")
|
||||||
if manga_counters_for_restore:
|
if manga_counters_for_restore:
|
||||||
self.log_signal.emit(f" Restoring manga counters: {manga_counters_for_restore}")
|
self.log_signal.emit(f" Restoring manga counters: {manga_counters_for_restore}")
|
||||||
|
|
||||||
@@ -2659,7 +2704,7 @@ class DownloaderApp (QWidget ):
|
|||||||
QMessageBox.warning(self, "Busy", "A download is already in progress.")
|
QMessageBox.warning(self, "Busy", "A download is already in progress.")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if not direct_api_url and self.favorite_download_queue and not self.is_processing_favorites_queue:
|
if not is_restore and not direct_api_url and self.favorite_download_queue and not self.is_processing_favorites_queue:
|
||||||
self.log_signal.emit(f"ℹ️ Detected {len(self.favorite_download_queue)} item(s) in the queue. Starting processing...")
|
self.log_signal.emit(f"ℹ️ Detected {len(self.favorite_download_queue)} item(s) in the queue. Starting processing...")
|
||||||
self.cancellation_message_logged_this_session = False
|
self.cancellation_message_logged_this_session = False
|
||||||
self._process_next_favorite_download()
|
self._process_next_favorite_download()
|
||||||
@@ -2716,7 +2761,7 @@ class DownloaderApp (QWidget ):
|
|||||||
effective_output_dir_for_run = os.path.normpath(main_ui_download_dir)
|
effective_output_dir_for_run = os.path.normpath(main_ui_download_dir)
|
||||||
|
|
||||||
if not is_restore:
|
if not is_restore:
|
||||||
self._create_initial_session_file(api_url, effective_output_dir_for_run)
|
self._create_initial_session_file(api_url, effective_output_dir_for_run, remaining_queue=self.favorite_download_queue)
|
||||||
|
|
||||||
self.download_history_candidates.clear()
|
self.download_history_candidates.clear()
|
||||||
self._update_button_states_and_connections()
|
self._update_button_states_and_connections()
|
||||||
@@ -2839,13 +2884,10 @@ class DownloaderApp (QWidget ):
|
|||||||
|
|
||||||
if backend_filter_mode == 'archive':
|
if backend_filter_mode == 'archive':
|
||||||
effective_skip_zip = False
|
effective_skip_zip = False
|
||||||
effective_skip_rar = False
|
|
||||||
else:
|
else:
|
||||||
effective_skip_zip = self.skip_zip_checkbox.isChecked()
|
effective_skip_zip = self.skip_zip_checkbox.isChecked()
|
||||||
effective_skip_rar = self.skip_rar_checkbox.isChecked()
|
|
||||||
if backend_filter_mode == 'audio':
|
if backend_filter_mode == 'audio':
|
||||||
effective_skip_zip = self.skip_zip_checkbox.isChecked()
|
effective_skip_zip = self.skip_zip_checkbox.isChecked()
|
||||||
effective_skip_rar = self.skip_rar_checkbox.isChecked()
|
|
||||||
|
|
||||||
if not api_url:
|
if not api_url:
|
||||||
QMessageBox.critical(self, "Input Error", "URL is required.")
|
QMessageBox.critical(self, "Input Error", "URL is required.")
|
||||||
@@ -3036,7 +3078,6 @@ class DownloaderApp (QWidget ):
|
|||||||
self.progress_label.setText(self._tr("progress_initializing_text", "Progress: Initializing..."))
|
self.progress_label.setText(self._tr("progress_initializing_text", "Progress: Initializing..."))
|
||||||
|
|
||||||
self.retryable_failed_files_info.clear()
|
self.retryable_failed_files_info.clear()
|
||||||
self.permanently_failed_files_for_dialog.clear()
|
|
||||||
self._update_error_button_count()
|
self._update_error_button_count()
|
||||||
|
|
||||||
manga_date_file_counter_ref_for_thread = None
|
manga_date_file_counter_ref_for_thread = None
|
||||||
@@ -3105,7 +3146,7 @@ class DownloaderApp (QWidget ):
|
|||||||
log_messages.extend([
|
log_messages.extend([
|
||||||
f" File Type Filter: {user_selected_filter_text} (Backend processing as: {backend_filter_mode})",
|
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" 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 Archives: {'.zip' if effective_skip_zip else ''}{', ' if effective_skip_zip else ''}{'None (Archive Mode)' if backend_filter_mode == 'archive' else ('None' if not (effective_skip_zip ) else '')}",
|
||||||
f" Skip Words Scope: {current_skip_words_scope.capitalize()}",
|
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" 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" Compress Images: {'Enabled' if compress_images else 'Disabled'}",
|
||||||
@@ -3157,7 +3198,6 @@ class DownloaderApp (QWidget ):
|
|||||||
'text_export_format': export_format_for_run,
|
'text_export_format': export_format_for_run,
|
||||||
'single_pdf_mode': self.single_pdf_setting,
|
'single_pdf_mode': self.single_pdf_setting,
|
||||||
'skip_zip': effective_skip_zip,
|
'skip_zip': effective_skip_zip,
|
||||||
'skip_rar': effective_skip_rar,
|
|
||||||
'use_subfolders': use_subfolders,
|
'use_subfolders': use_subfolders,
|
||||||
'use_post_subfolders': use_post_subfolders,
|
'use_post_subfolders': use_post_subfolders,
|
||||||
'compress_images': compress_images,
|
'compress_images': compress_images,
|
||||||
@@ -3206,6 +3246,7 @@ class DownloaderApp (QWidget ):
|
|||||||
'downloaded_hash_counts_lock': self.downloaded_hash_counts_lock,
|
'downloaded_hash_counts_lock': self.downloaded_hash_counts_lock,
|
||||||
'skip_current_file_flag': None,
|
'skip_current_file_flag': None,
|
||||||
'processed_post_ids': processed_post_ids_for_restore,
|
'processed_post_ids': processed_post_ids_for_restore,
|
||||||
|
'start_offset': start_offset_for_restore,
|
||||||
}
|
}
|
||||||
|
|
||||||
args_template['override_output_dir'] = override_output_dir
|
args_template['override_output_dir'] = override_output_dir
|
||||||
@@ -3218,12 +3259,12 @@ class DownloaderApp (QWidget ):
|
|||||||
self.log_signal.emit(f" Initializing single-threaded {'link extraction' if extract_links_only else 'download'}...")
|
self.log_signal.emit(f" Initializing single-threaded {'link extraction' if extract_links_only else 'download'}...")
|
||||||
dt_expected_keys = [
|
dt_expected_keys = [
|
||||||
'api_url_input', 'output_dir', 'known_names_copy', 'cancellation_event',
|
'api_url_input', 'output_dir', 'known_names_copy', 'cancellation_event',
|
||||||
'filter_character_list', 'filter_mode', 'skip_zip', 'skip_rar',
|
'filter_character_list', 'filter_mode', 'skip_zip',
|
||||||
'use_subfolders', 'use_post_subfolders', 'custom_folder_name',
|
'use_subfolders', 'use_post_subfolders', 'custom_folder_name',
|
||||||
'compress_images', 'download_thumbnails', 'service', 'user_id',
|
'compress_images', 'download_thumbnails', 'service', 'user_id',
|
||||||
'downloaded_files', 'downloaded_file_hashes', 'pause_event', 'remove_from_filename_words_list',
|
'downloaded_files', 'downloaded_file_hashes', 'pause_event', 'remove_from_filename_words_list',
|
||||||
'downloaded_files_lock', 'downloaded_file_hashes_lock', 'dynamic_character_filter_holder', 'session_file_path',
|
'downloaded_files_lock', 'downloaded_file_hashes_lock', 'dynamic_character_filter_holder', 'session_file_path',
|
||||||
'session_lock',
|
'session_lock', 'start_offset',
|
||||||
'skip_words_list', 'skip_words_scope', 'char_filter_scope',
|
'skip_words_list', 'skip_words_scope', 'char_filter_scope',
|
||||||
'show_external_links', 'extract_links_only', 'num_file_threads_for_worker',
|
'show_external_links', 'extract_links_only', 'num_file_threads_for_worker',
|
||||||
'start_page', 'end_page', 'target_post_id_from_initial_url',
|
'start_page', 'end_page', 'target_post_id_from_initial_url',
|
||||||
@@ -3260,6 +3301,13 @@ class DownloaderApp (QWidget ):
|
|||||||
self._clear_session_and_reset_ui()
|
self._clear_session_and_reset_ui()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
remaining_queue_from_session = self.interrupted_session_data.get("remaining_queue", [])
|
||||||
|
if remaining_queue_from_session:
|
||||||
|
self.favorite_download_queue.clear()
|
||||||
|
self.favorite_download_queue.extend(remaining_queue_from_session)
|
||||||
|
self.is_processing_favorites_queue = True
|
||||||
|
self.log_signal.emit(f"ℹ️ Restored {len(self.favorite_download_queue)} item(s) to the download queue. Processing will continue automatically.")
|
||||||
|
|
||||||
self.log_signal.emit("🔄 Preparing to restore download session...")
|
self.log_signal.emit("🔄 Preparing to restore download session...")
|
||||||
|
|
||||||
settings = self.interrupted_session_data.get("ui_settings", {})
|
settings = self.interrupted_session_data.get("ui_settings", {})
|
||||||
@@ -3355,6 +3403,13 @@ class DownloaderApp (QWidget ):
|
|||||||
self .cancellation_event .set ()
|
self .cancellation_event .set ()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
if 'output_dir' in worker_init_args:
|
||||||
|
worker_init_args['download_root'] = worker_init_args.pop('output_dir')
|
||||||
|
if 'initial_target_post_id' in worker_init_args:
|
||||||
|
worker_init_args['target_post_id_from_initial_url'] = worker_init_args.pop('initial_target_post_id')
|
||||||
|
if 'filter_character_list_objects_initial' in worker_init_args:
|
||||||
|
worker_init_args['filter_character_list'] = worker_init_args.pop('filter_character_list_objects_initial')
|
||||||
|
|
||||||
try :
|
try :
|
||||||
worker_instance =PostProcessorWorker (**worker_init_args )
|
worker_instance =PostProcessorWorker (**worker_init_args )
|
||||||
if self .thread_pool :
|
if self .thread_pool :
|
||||||
@@ -3412,6 +3467,27 @@ class DownloaderApp (QWidget ):
|
|||||||
elif filter_mode == 'audio' and hasattr(self, 'radio_only_audio'): self.radio_only_audio.setChecked(True)
|
elif filter_mode == 'audio' and hasattr(self, 'radio_only_audio'): self.radio_only_audio.setChecked(True)
|
||||||
else: self.radio_all.setChecked(True)
|
else: self.radio_all.setChecked(True)
|
||||||
|
|
||||||
|
self.keep_duplicates_mode = settings.get('keep_duplicates_mode', DUPLICATE_HANDLING_HASH)
|
||||||
|
self.keep_duplicates_limit = settings.get('keep_duplicates_limit', 0)
|
||||||
|
# Visually update the checkbox based on the restored mode
|
||||||
|
if hasattr(self, 'keep_duplicates_checkbox'):
|
||||||
|
is_keep_mode = (self.keep_duplicates_mode == DUPLICATE_HANDLING_KEEP_ALL)
|
||||||
|
self.keep_duplicates_checkbox.setChecked(is_keep_mode)
|
||||||
|
|
||||||
|
# Restore "More" dialog settings
|
||||||
|
self.more_filter_scope = settings.get('more_filter_scope')
|
||||||
|
self.text_export_format = settings.get('text_export_format', 'pdf')
|
||||||
|
self.single_pdf_setting = settings.get('single_pdf_setting', False)
|
||||||
|
|
||||||
|
# Visually update the "More" button's text to reflect the restored settings
|
||||||
|
if self.radio_more.isChecked() and self.more_filter_scope:
|
||||||
|
from .dialogs.MoreOptionsDialog import MoreOptionsDialog
|
||||||
|
scope_text = "Comments" if self.more_filter_scope == MoreOptionsDialog.SCOPE_COMMENTS else "Description"
|
||||||
|
format_display = f" ({self.text_export_format.upper()})"
|
||||||
|
if self.single_pdf_setting:
|
||||||
|
format_display = " (Single PDF)"
|
||||||
|
self.radio_more.setText(f"{scope_text}{format_display}")
|
||||||
|
|
||||||
# Toggle button states
|
# Toggle button states
|
||||||
self.skip_words_scope = settings.get('skip_words_scope', SKIP_SCOPE_POSTS)
|
self.skip_words_scope = settings.get('skip_words_scope', SKIP_SCOPE_POSTS)
|
||||||
self.char_filter_scope = settings.get('char_filter_scope', CHAR_SCOPE_TITLE)
|
self.char_filter_scope = settings.get('char_filter_scope', CHAR_SCOPE_TITLE)
|
||||||
@@ -3440,9 +3516,6 @@ class DownloaderApp (QWidget ):
|
|||||||
self.all_kept_original_filenames = []
|
self.all_kept_original_filenames = []
|
||||||
self.is_fetcher_thread_running = True
|
self.is_fetcher_thread_running = True
|
||||||
|
|
||||||
# --- START OF FIX ---
|
|
||||||
# Bundle all arguments for the fetcher thread into a single dictionary
|
|
||||||
# to ensure the correct number of arguments are passed.
|
|
||||||
fetcher_thread_args = {
|
fetcher_thread_args = {
|
||||||
'api_url': kwargs.get('api_url_input'),
|
'api_url': kwargs.get('api_url_input'),
|
||||||
'worker_args_template': kwargs,
|
'worker_args_template': kwargs,
|
||||||
@@ -3452,11 +3525,10 @@ class DownloaderApp (QWidget ):
|
|||||||
|
|
||||||
fetcher_thread = threading.Thread(
|
fetcher_thread = threading.Thread(
|
||||||
target=self._fetch_and_queue_posts,
|
target=self._fetch_and_queue_posts,
|
||||||
args=(fetcher_thread_args,), # Pass the single dictionary as an argument
|
args=(fetcher_thread_args,),
|
||||||
daemon=True,
|
daemon=True,
|
||||||
name="PostFetcher"
|
name="PostFetcher"
|
||||||
)
|
)
|
||||||
# --- END OF FIX ---
|
|
||||||
|
|
||||||
fetcher_thread.start()
|
fetcher_thread.start()
|
||||||
self.log_signal.emit(f"✅ Post fetcher thread started. {num_post_workers} post worker threads initializing...")
|
self.log_signal.emit(f"✅ Post fetcher thread started. {num_post_workers} post worker threads initializing...")
|
||||||
@@ -3465,70 +3537,133 @@ class DownloaderApp (QWidget ):
|
|||||||
def _fetch_and_queue_posts(self, fetcher_args):
|
def _fetch_and_queue_posts(self, fetcher_args):
|
||||||
"""
|
"""
|
||||||
Fetches post data and submits tasks to the pool.
|
Fetches post data and submits tasks to the pool.
|
||||||
This version unpacks arguments from a single dictionary.
|
This version is corrected to handle single-post fetches directly
|
||||||
|
in multi-threaded mode.
|
||||||
"""
|
"""
|
||||||
global PostProcessorWorker, download_from_api
|
global PostProcessorWorker, download_from_api, requests, json, traceback, urlparse
|
||||||
|
|
||||||
# --- START OF FIX ---
|
|
||||||
# Unpack arguments from the dictionary passed by the thread
|
# Unpack arguments from the dictionary passed by the thread
|
||||||
api_url_input_for_fetcher = fetcher_args['api_url']
|
api_url_input_for_fetcher = fetcher_args['api_url']
|
||||||
worker_args_template = fetcher_args['worker_args_template']
|
worker_args_template = fetcher_args['worker_args_template']
|
||||||
num_post_workers = fetcher_args['num_post_workers']
|
processed_post_ids_set = set(fetcher_args.get('processed_post_ids', []))
|
||||||
processed_post_ids = fetcher_args['processed_post_ids']
|
start_offset = fetcher_args.get('start_offset', 0)
|
||||||
# --- END OF FIX ---
|
target_post_id = worker_args_template.get('target_post_id_from_initial_url') # Get the target post ID
|
||||||
|
logger_func = lambda msg: self.log_signal.emit(f"[Fetcher] {msg}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
post_generator = download_from_api(
|
# Prepare common variables for the fetcher thread
|
||||||
api_url_input_for_fetcher,
|
service = worker_args_template.get('service')
|
||||||
logger=lambda msg: self.log_signal.emit(f"[Fetcher] {msg}"),
|
user_id = worker_args_template.get('user_id')
|
||||||
start_page=worker_args_template.get('start_page'),
|
cancellation_event = self.cancellation_event
|
||||||
end_page=worker_args_template.get('end_page'),
|
pause_event = self.pause_event
|
||||||
manga_mode=worker_args_template.get('manga_mode_active', False),
|
session_lock = self.session_lock
|
||||||
cancellation_event=self.cancellation_event,
|
session_file_path = self.session_file_path
|
||||||
pause_event=self.pause_event,
|
parsed_api_url = urlparse(api_url_input_for_fetcher)
|
||||||
use_cookie=worker_args_template.get('use_cookie'),
|
headers = {'User-Agent': 'Mozilla/5.0', 'Referer': f"https://{parsed_api_url.netloc}/"}
|
||||||
cookie_text=worker_args_template.get('cookie_text'),
|
cookies = prepare_cookies_for_request(
|
||||||
selected_cookie_file=worker_args_template.get('selected_cookie_file'),
|
worker_args_template.get('use_cookie'),
|
||||||
app_base_dir=worker_args_template.get('app_base_dir'),
|
worker_args_template.get('cookie_text'),
|
||||||
manga_filename_style_for_sort_check=worker_args_template.get('manga_filename_style'),
|
worker_args_template.get('selected_cookie_file'),
|
||||||
processed_post_ids=processed_post_ids
|
worker_args_template.get('app_base_dir'),
|
||||||
|
logger_func
|
||||||
)
|
)
|
||||||
|
|
||||||
ppw_expected_keys = [
|
if target_post_id:
|
||||||
'post_data','download_root','known_names','filter_character_list','unwanted_keywords',
|
logger_func(f"Mode: Single Post. Attempting direct fetch for post ID: {target_post_id}")
|
||||||
'filter_mode','skip_zip','skip_rar','use_subfolders','use_post_subfolders',
|
post_api_url = f"https://{parsed_api_url.netloc}/api/v1/{service}/user/{user_id}/post/{target_post_id}"
|
||||||
'target_post_id_from_initial_url','custom_folder_name','compress_images','emitter',
|
|
||||||
'pause_event','download_thumbnails','service','user_id','api_url_input',
|
try:
|
||||||
'cancellation_event','downloaded_files','downloaded_file_hashes','downloaded_files_lock',
|
response = requests.get(post_api_url, headers=headers, cookies=cookies, timeout=(15, 60))
|
||||||
'downloaded_file_hashes_lock','remove_from_filename_words_list','dynamic_character_filter_holder',
|
response.raise_for_status()
|
||||||
'skip_words_list','skip_words_scope','char_filter_scope','show_external_links',
|
single_post_data = response.json()
|
||||||
'extract_links_only','allow_multipart_download','use_cookie','cookie_text',
|
|
||||||
'app_base_dir','selected_cookie_file','override_output_dir','num_file_threads',
|
if isinstance(single_post_data, list) and single_post_data:
|
||||||
'skip_current_file_flag','manga_date_file_counter_ref','scan_content_for_images',
|
single_post_data = single_post_data[0]
|
||||||
'manga_mode_active','manga_filename_style','manga_date_prefix','text_only_scope',
|
|
||||||
'text_export_format', 'single_pdf_mode',
|
if not isinstance(single_post_data, dict):
|
||||||
'use_date_prefix_for_subfolder','keep_in_post_duplicates','keep_duplicates_mode','manga_global_file_counter_ref',
|
raise ValueError(f"Expected a dictionary for post data, but got {type(single_post_data)}")
|
||||||
'creator_download_folder_ignore_words','session_file_path','project_root_dir','session_lock',
|
|
||||||
'processed_post_ids', 'keep_duplicates_limit', 'downloaded_hash_counts', 'downloaded_hash_counts_lock'
|
# Set total posts to 1 and submit the single job to the worker pool
|
||||||
|
self.total_posts_to_process = 1
|
||||||
|
self.overall_progress_signal.emit(1, 0)
|
||||||
|
|
||||||
|
ppw_expected_keys = list(PostProcessorWorker.__init__.__code__.co_varnames)[1:]
|
||||||
|
self._submit_post_to_worker_pool(
|
||||||
|
single_post_data,
|
||||||
|
worker_args_template,
|
||||||
|
worker_args_template.get('num_file_threads_for_worker', 1),
|
||||||
|
worker_args_template.get('emitter'),
|
||||||
|
ppw_expected_keys,
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
except (requests.RequestException, json.JSONDecodeError, ValueError) as e:
|
||||||
|
logger_func(f"❌ Failed to fetch single post directly: {e}. Aborting.")
|
||||||
|
|
||||||
|
return
|
||||||
|
offset = start_offset
|
||||||
|
page_size = 50
|
||||||
|
|
||||||
|
while not cancellation_event.is_set():
|
||||||
|
while pause_event.is_set():
|
||||||
|
time.sleep(0.5)
|
||||||
|
if cancellation_event.is_set(): break
|
||||||
|
if cancellation_event.is_set(): break
|
||||||
|
|
||||||
|
api_url = f"https://{parsed_api_url.netloc}/api/v1/{service}/user/{user_id}?o={offset}"
|
||||||
|
logger_func(f"Fetching post list: {api_url} (Page approx. {offset // page_size + 1})")
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.get(api_url, headers=headers, cookies=cookies, timeout=20)
|
||||||
|
response.raise_for_status()
|
||||||
|
posts_batch_from_api = response.json()
|
||||||
|
except (requests.RequestException, json.JSONDecodeError) as e:
|
||||||
|
logger_func(f"❌ API Error fetching posts: {e}. Stopping fetch.")
|
||||||
|
break
|
||||||
|
|
||||||
|
if not posts_batch_from_api:
|
||||||
|
logger_func("✅ Reached end of posts (API returned no more content).")
|
||||||
|
break
|
||||||
|
|
||||||
|
new_posts_to_process = [
|
||||||
|
post for post in posts_batch_from_api if post.get('id') not in processed_post_ids_set
|
||||||
]
|
]
|
||||||
|
|
||||||
num_file_dl_threads_for_each_worker = worker_args_template.get('num_file_threads_for_worker', 1)
|
num_skipped = len(posts_batch_from_api) - len(new_posts_to_process)
|
||||||
emitter_for_worker = worker_args_template.get('emitter')
|
if num_skipped > 0:
|
||||||
|
logger_func(f" Skipped {num_skipped} already processed post(s) from this page.")
|
||||||
|
|
||||||
for posts_batch in post_generator:
|
if new_posts_to_process:
|
||||||
if self.cancellation_event.is_set():
|
ppw_expected_keys = list(PostProcessorWorker.__init__.__code__.co_varnames)[1:]
|
||||||
|
num_file_dl_threads = worker_args_template.get('num_file_threads_for_worker', 1)
|
||||||
|
emitter = worker_args_template.get('emitter')
|
||||||
|
|
||||||
|
for post_data in new_posts_to_process:
|
||||||
|
if cancellation_event.is_set():
|
||||||
break
|
break
|
||||||
if isinstance(posts_batch, list) and posts_batch:
|
self._submit_post_to_worker_pool(post_data, worker_args_template, num_file_dl_threads, emitter, ppw_expected_keys, {})
|
||||||
for post_data_item in posts_batch:
|
|
||||||
self._submit_post_to_worker_pool(post_data_item, worker_args_template, num_file_dl_threads_for_each_worker, emitter_for_worker, ppw_expected_keys, {})
|
self.total_posts_to_process += len(new_posts_to_process)
|
||||||
self.total_posts_to_process += len(posts_batch)
|
|
||||||
self.overall_progress_signal.emit(self.total_posts_to_process, self.processed_posts_count)
|
self.overall_progress_signal.emit(self.total_posts_to_process, self.processed_posts_count)
|
||||||
|
|
||||||
|
next_offset = offset + page_size
|
||||||
|
with session_lock:
|
||||||
|
if os.path.exists(session_file_path):
|
||||||
|
try:
|
||||||
|
with open(session_file_path, 'r', encoding='utf-8') as f:
|
||||||
|
session_data = json.load(f)
|
||||||
|
session_data['download_state']['last_processed_offset'] = next_offset
|
||||||
|
self._save_session_file(session_data)
|
||||||
|
except (json.JSONDecodeError, KeyError, OSError) as e:
|
||||||
|
logger_func(f"⚠️ Could not update session offset: {e}")
|
||||||
|
|
||||||
|
offset = next_offset
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log_signal.emit(f"❌ Error during post fetching: {e}\n{traceback.format_exc(limit=2)}")
|
logger_func(f"❌ Critical error during post fetching: {e}\n{traceback.format_exc(limit=2)}")
|
||||||
finally:
|
finally:
|
||||||
self.is_fetcher_thread_running = False
|
self.is_fetcher_thread_running = False
|
||||||
self.log_signal.emit("ℹ️ Post fetcher thread has finished submitting tasks.")
|
logger_func("ℹ️ Post fetcher thread has finished submitting tasks.")
|
||||||
|
self._check_if_all_work_is_done()
|
||||||
|
|
||||||
def _handle_worker_result(self, result_tuple: tuple):
|
def _handle_worker_result(self, result_tuple: tuple):
|
||||||
"""
|
"""
|
||||||
@@ -3554,13 +3689,14 @@ class DownloaderApp (QWidget ):
|
|||||||
|
|
||||||
# Other result handling
|
# Other result handling
|
||||||
if history_data: self._add_to_history_candidates(history_data)
|
if history_data: self._add_to_history_candidates(history_data)
|
||||||
# You can add handling for 'retryable' here if needed in the future
|
|
||||||
|
|
||||||
self.overall_progress_signal.emit(self.total_posts_to_process, self.processed_posts_count)
|
self.overall_progress_signal.emit(self.total_posts_to_process, self.processed_posts_count)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log_signal.emit(f"❌ Error in _handle_worker_result: {e}\n{traceback.format_exc(limit=2)}")
|
self.log_signal.emit(f"❌ Error in _handle_worker_result: {e}\n{traceback.format_exc(limit=2)}")
|
||||||
|
|
||||||
|
self._check_if_all_work_is_done()
|
||||||
|
|
||||||
if not self.is_fetcher_thread_running and self.processed_posts_count >= self.total_posts_to_process:
|
if not self.is_fetcher_thread_running and self.processed_posts_count >= self.total_posts_to_process:
|
||||||
self.finished_signal.emit(self.download_counter, self.skip_counter, self.cancellation_event.is_set(), self.all_kept_original_filenames)
|
self.finished_signal.emit(self.download_counter, self.skip_counter, self.cancellation_event.is_set(), self.all_kept_original_filenames)
|
||||||
|
|
||||||
@@ -3595,12 +3731,8 @@ class DownloaderApp (QWidget ):
|
|||||||
|
|
||||||
font_path = os.path.join(self.app_base_dir, 'data', 'dejavu-sans', 'DejaVuSans.ttf')
|
font_path = os.path.join(self.app_base_dir, 'data', 'dejavu-sans', 'DejaVuSans.ttf')
|
||||||
|
|
||||||
# vvv THIS IS THE KEY CHANGE vvv
|
|
||||||
# Sort content by the 'published' date. ISO-formatted dates sort correctly as strings.
|
|
||||||
# Use a fallback value 'Z' to place any posts without a date at the end.
|
|
||||||
self.log_signal.emit(" Sorting collected posts by date (oldest first)...")
|
self.log_signal.emit(" Sorting collected posts by date (oldest first)...")
|
||||||
sorted_content = sorted(posts_content_data, key=lambda x: x.get('published', 'Z'))
|
sorted_content = sorted(posts_content_data, key=lambda x: x.get('published', 'Z'))
|
||||||
# ^^^ END OF KEY CHANGE ^^^
|
|
||||||
|
|
||||||
create_single_pdf_from_content(sorted_content, filepath, font_path, logger=self.log_signal.emit)
|
create_single_pdf_from_content(sorted_content, filepath, font_path, logger=self.log_signal.emit)
|
||||||
self.log_signal.emit("="*40)
|
self.log_signal.emit("="*40)
|
||||||
@@ -3659,7 +3791,7 @@ class DownloaderApp (QWidget ):
|
|||||||
self .remove_from_filename_input ,
|
self .remove_from_filename_input ,
|
||||||
self .radio_all ,self .radio_images ,self .radio_videos ,
|
self .radio_all ,self .radio_images ,self .radio_videos ,
|
||||||
self .radio_only_archives ,self .radio_only_links ,
|
self .radio_only_archives ,self .radio_only_links ,
|
||||||
self .skip_zip_checkbox ,self .skip_rar_checkbox ,
|
self .skip_zip_checkbox ,
|
||||||
self .download_thumbnails_checkbox ,self .compress_images_checkbox ,
|
self .download_thumbnails_checkbox ,self .compress_images_checkbox ,
|
||||||
self .use_subfolders_checkbox ,self .use_subfolder_per_post_checkbox ,
|
self .use_subfolders_checkbox ,self .use_subfolder_per_post_checkbox ,
|
||||||
self .manga_mode_checkbox ,
|
self .manga_mode_checkbox ,
|
||||||
@@ -3682,7 +3814,7 @@ class DownloaderApp (QWidget ):
|
|||||||
self .custom_folder_label ,self .custom_folder_input ,
|
self .custom_folder_label ,self .custom_folder_input ,
|
||||||
self .skip_words_input ,self .skip_scope_toggle_button ,self .remove_from_filename_input ,
|
self .skip_words_input ,self .skip_scope_toggle_button ,self .remove_from_filename_input ,
|
||||||
self .radio_all ,self .radio_images ,self .radio_videos ,self .radio_only_archives ,self .radio_only_links ,
|
self .radio_all ,self .radio_images ,self .radio_videos ,self .radio_only_archives ,self .radio_only_links ,
|
||||||
self .skip_zip_checkbox ,self .skip_rar_checkbox ,self .download_thumbnails_checkbox ,self .compress_images_checkbox ,
|
self .skip_zip_checkbox , self .download_thumbnails_checkbox ,self .compress_images_checkbox ,
|
||||||
self .use_subfolders_checkbox ,self .use_subfolder_per_post_checkbox ,self .scan_content_images_checkbox ,
|
self .use_subfolders_checkbox ,self .use_subfolder_per_post_checkbox ,self .scan_content_images_checkbox ,
|
||||||
self .use_multithreading_checkbox ,self .thread_count_input ,self .thread_count_label ,
|
self .use_multithreading_checkbox ,self .thread_count_input ,self .thread_count_label ,
|
||||||
self .favorite_mode_checkbox ,
|
self .favorite_mode_checkbox ,
|
||||||
@@ -3801,7 +3933,7 @@ class DownloaderApp (QWidget ):
|
|||||||
self .skip_words_input .clear ();self .start_page_input .clear ();self .end_page_input .clear ();self .new_char_input .clear ();
|
self .skip_words_input .clear ();self .start_page_input .clear ();self .end_page_input .clear ();self .new_char_input .clear ();
|
||||||
if hasattr (self ,'remove_from_filename_input'):self .remove_from_filename_input .clear ()
|
if hasattr (self ,'remove_from_filename_input'):self .remove_from_filename_input .clear ()
|
||||||
self .character_search_input .clear ();self .thread_count_input .setText ("4");self .radio_all .setChecked (True );
|
self .character_search_input .clear ();self .thread_count_input .setText ("4");self .radio_all .setChecked (True );
|
||||||
self .skip_zip_checkbox .setChecked (True );self .skip_rar_checkbox .setChecked (True );self .download_thumbnails_checkbox .setChecked (False );
|
self .skip_zip_checkbox .setChecked (True );self .download_thumbnails_checkbox .setChecked (False );
|
||||||
self .compress_images_checkbox .setChecked (False );self .use_subfolders_checkbox .setChecked (True );
|
self .compress_images_checkbox .setChecked (False );self .use_subfolders_checkbox .setChecked (True );
|
||||||
self .use_subfolder_per_post_checkbox .setChecked (False );self .use_multithreading_checkbox .setChecked (True );
|
self .use_subfolder_per_post_checkbox .setChecked (False );self .use_multithreading_checkbox .setChecked (True );
|
||||||
if self .favorite_mode_checkbox :self .favorite_mode_checkbox .setChecked (False )
|
if self .favorite_mode_checkbox :self .favorite_mode_checkbox .setChecked (False )
|
||||||
@@ -3941,17 +4073,22 @@ class DownloaderApp (QWidget ):
|
|||||||
return "coomer.su"
|
return "coomer.su"
|
||||||
return "kemono.su"
|
return "kemono.su"
|
||||||
|
|
||||||
|
|
||||||
def download_finished(self, total_downloaded, total_skipped, cancelled_by_user, kept_original_names_list=None):
|
def download_finished(self, total_downloaded, total_skipped, cancelled_by_user, kept_original_names_list=None):
|
||||||
if self.is_finishing:
|
if self.is_finishing:
|
||||||
return
|
return
|
||||||
self.is_finishing = True
|
self.is_finishing = True
|
||||||
|
|
||||||
self.log_signal.emit("🏁 All fetcher and worker tasks complete.")
|
self.log_signal.emit("🏁 Download of current item complete.")
|
||||||
|
|
||||||
if kept_original_names_list is None :
|
if self.is_processing_favorites_queue and self.favorite_download_queue:
|
||||||
kept_original_names_list =list (self .all_kept_original_filenames )if hasattr (self ,'all_kept_original_filenames')else []
|
self.log_signal.emit("✅ Item finished. Processing next in queue...")
|
||||||
if kept_original_names_list is None :
|
self._process_next_favorite_download()
|
||||||
kept_original_names_list =[]
|
return
|
||||||
|
|
||||||
|
if self.is_processing_favorites_queue:
|
||||||
|
self.is_processing_favorites_queue = False
|
||||||
|
self.log_signal.emit("✅ All items from the download queue have been processed.")
|
||||||
|
|
||||||
if not cancelled_by_user and not self.retryable_failed_files_info:
|
if not cancelled_by_user and not self.retryable_failed_files_info:
|
||||||
self._clear_session_file()
|
self._clear_session_file()
|
||||||
@@ -3968,25 +4105,28 @@ class DownloaderApp (QWidget ):
|
|||||||
summary_log += f"\n🏁 Download {status_message}!\n Summary: Downloaded Files={total_downloaded}, Skipped Files={total_skipped}\n"
|
summary_log += f"\n🏁 Download {status_message}!\n Summary: Downloaded Files={total_downloaded}, Skipped Files={total_skipped}\n"
|
||||||
summary_log += "=" * 40
|
summary_log += "=" * 40
|
||||||
self.log_signal.emit(summary_log)
|
self.log_signal.emit(summary_log)
|
||||||
|
self.log_signal.emit("")
|
||||||
|
|
||||||
# Safely shut down the thread pool now that all work is done.
|
|
||||||
if self.thread_pool:
|
if self.thread_pool:
|
||||||
self.log_signal.emit(" Shutting down worker thread pool...")
|
self.log_signal.emit(" Shutting down worker thread pool...")
|
||||||
self.thread_pool.shutdown(wait=False)
|
self.thread_pool.shutdown(wait=False)
|
||||||
self.thread_pool = None
|
self.thread_pool = None
|
||||||
self.log_signal.emit(" Thread pool shut down.")
|
self.log_signal.emit(" Thread pool shut down.")
|
||||||
|
|
||||||
try:
|
|
||||||
if self.single_pdf_setting and self.session_temp_files and not cancelled_by_user:
|
if self.single_pdf_setting and self.session_temp_files and not cancelled_by_user:
|
||||||
|
try:
|
||||||
self._trigger_single_pdf_creation()
|
self._trigger_single_pdf_creation()
|
||||||
finally:
|
finally:
|
||||||
# This ensures cleanup happens even if PDF creation fails or is cancelled
|
self._cleanup_temp_files()
|
||||||
|
self.single_pdf_setting = False
|
||||||
|
else:
|
||||||
self._cleanup_temp_files()
|
self._cleanup_temp_files()
|
||||||
self.single_pdf_setting = False
|
self.single_pdf_setting = False
|
||||||
|
|
||||||
# Reset session state for the next run
|
if kept_original_names_list is None:
|
||||||
self.session_temp_files = []
|
kept_original_names_list = list(self.all_kept_original_filenames) if hasattr(self, 'all_kept_original_filenames') else []
|
||||||
self.single_pdf_setting = False
|
if kept_original_names_list is None:
|
||||||
|
kept_original_names_list = []
|
||||||
|
|
||||||
if kept_original_names_list:
|
if kept_original_names_list:
|
||||||
intro_msg = (
|
intro_msg = (
|
||||||
@@ -3995,12 +4135,10 @@ class DownloaderApp (QWidget ):
|
|||||||
"(after the first file) kept their <b>original names</b>:</p>"
|
"(after the first file) kept their <b>original names</b>:</p>"
|
||||||
)
|
)
|
||||||
self.log_signal.emit(intro_msg)
|
self.log_signal.emit(intro_msg)
|
||||||
|
|
||||||
html_list_items = "<ul>"
|
html_list_items = "<ul>"
|
||||||
for name in kept_original_names_list:
|
for name in kept_original_names_list:
|
||||||
html_list_items += f"<li><b>{name}</b></li>"
|
html_list_items += f"<li><b>{name}</b></li>"
|
||||||
html_list_items += "</ul>"
|
html_list_items += "</ul>"
|
||||||
|
|
||||||
self.log_signal.emit(HTML_PREFIX + html_list_items)
|
self.log_signal.emit(HTML_PREFIX + html_list_items)
|
||||||
self.log_signal.emit("=" * 40)
|
self.log_signal.emit("=" * 40)
|
||||||
|
|
||||||
@@ -4012,19 +4150,14 @@ class DownloaderApp (QWidget ):
|
|||||||
if hasattr(self.download_thread, 'receive_add_character_result'): self.character_prompt_response_signal.disconnect(self.download_thread.receive_add_character_result)
|
if hasattr(self.download_thread, 'receive_add_character_result'): self.character_prompt_response_signal.disconnect(self.download_thread.receive_add_character_result)
|
||||||
if hasattr(self.download_thread, 'external_link_signal'): self.download_thread.external_link_signal.disconnect(self.handle_external_link_signal)
|
if hasattr(self.download_thread, 'external_link_signal'): self.download_thread.external_link_signal.disconnect(self.handle_external_link_signal)
|
||||||
if hasattr(self.download_thread, 'file_progress_signal'): self.download_thread.file_progress_signal.disconnect(self.update_file_progress_display)
|
if hasattr(self.download_thread, 'file_progress_signal'): self.download_thread.file_progress_signal.disconnect(self.update_file_progress_display)
|
||||||
if hasattr (self .download_thread ,'missed_character_post_signal'):
|
if hasattr(self.download_thread, 'missed_character_post_signal'): self.download_thread.missed_character_post_signal.disconnect(self.handle_missed_character_post)
|
||||||
self .download_thread .missed_character_post_signal .disconnect (self .handle_missed_character_post )
|
if hasattr(self.download_thread, 'retryable_file_failed_signal'): self.download_thread.retryable_file_failed_signal.disconnect(self._handle_retryable_file_failure)
|
||||||
if hasattr (self .download_thread ,'retryable_file_failed_signal'):
|
if hasattr(self.download_thread, 'file_successfully_downloaded_signal'): self.download_thread.file_successfully_downloaded_signal.disconnect(self._handle_actual_file_downloaded)
|
||||||
self .download_thread .retryable_file_failed_signal .disconnect (self ._handle_retryable_file_failure )
|
if hasattr(self.download_thread, 'post_processed_for_history_signal'): self.download_thread.post_processed_for_history_signal.disconnect(self._add_to_history_candidates)
|
||||||
if hasattr (self .download_thread ,'file_successfully_downloaded_signal'):
|
|
||||||
self .download_thread .file_successfully_downloaded_signal .disconnect (self ._handle_actual_file_downloaded )
|
|
||||||
if hasattr (self .download_thread ,'post_processed_for_history_signal'):
|
|
||||||
self .download_thread .post_processed_for_history_signal .disconnect (self ._add_to_history_candidates )
|
|
||||||
except (TypeError, RuntimeError) as e:
|
except (TypeError, RuntimeError) as e:
|
||||||
self.log_signal.emit(f"ℹ️ Note during single-thread signal disconnection: {e}")
|
self.log_signal.emit(f"ℹ️ Note during single-thread signal disconnection: {e}")
|
||||||
|
|
||||||
if not self.download_thread.isRunning():
|
if not self.download_thread.isRunning():
|
||||||
|
|
||||||
if self.download_thread:
|
if self.download_thread:
|
||||||
self.download_thread.deleteLater()
|
self.download_thread.deleteLater()
|
||||||
self.download_thread = None
|
self.download_thread = None
|
||||||
@@ -4035,17 +4168,7 @@ class DownloaderApp (QWidget ):
|
|||||||
f"{total_skipped} {self._tr('files_skipped_label', 'skipped')}."
|
f"{total_skipped} {self._tr('files_skipped_label', 'skipped')}."
|
||||||
)
|
)
|
||||||
self.file_progress_label.setText("")
|
self.file_progress_label.setText("")
|
||||||
if not cancelled_by_user :self ._try_process_next_external_link ()
|
|
||||||
|
|
||||||
if self .thread_pool :
|
|
||||||
self .log_signal .emit (" Ensuring worker thread pool is shut down...")
|
|
||||||
self .thread_pool .shutdown (wait =True ,cancel_futures =True )
|
|
||||||
self .thread_pool =None
|
|
||||||
|
|
||||||
self .active_futures =[]
|
|
||||||
if self .pause_event :self .pause_event .clear ()
|
|
||||||
self .cancel_btn .setEnabled (False )
|
|
||||||
self .is_paused =False
|
|
||||||
if not cancelled_by_user and self.retryable_failed_files_info:
|
if not cancelled_by_user and self.retryable_failed_files_info:
|
||||||
num_failed = len(self.retryable_failed_files_info)
|
num_failed = len(self.retryable_failed_files_info)
|
||||||
reply = QMessageBox.question(self, "Retry Failed Downloads?",
|
reply = QMessageBox.question(self, "Retry Failed Downloads?",
|
||||||
@@ -4065,15 +4188,8 @@ class DownloaderApp (QWidget ):
|
|||||||
|
|
||||||
self.is_fetcher_thread_running = False
|
self.is_fetcher_thread_running = False
|
||||||
|
|
||||||
if self .is_processing_favorites_queue :
|
|
||||||
if not self .favorite_download_queue :
|
|
||||||
self .is_processing_favorites_queue =False
|
|
||||||
self .log_signal .emit (f"✅ All {self .current_processing_favorite_item_info .get ('type','item')} downloads from favorite queue have been processed.")
|
|
||||||
self .set_ui_enabled (not self ._is_download_active ())
|
|
||||||
else :
|
|
||||||
self ._process_next_favorite_download ()
|
|
||||||
else :
|
|
||||||
self.set_ui_enabled(True)
|
self.set_ui_enabled(True)
|
||||||
|
self._update_button_states_and_connections()
|
||||||
self.cancellation_message_logged_this_session = False
|
self.cancellation_message_logged_this_session = False
|
||||||
|
|
||||||
def _handle_keep_duplicates_toggled(self, checked):
|
def _handle_keep_duplicates_toggled(self, checked):
|
||||||
@@ -4167,7 +4283,6 @@ class DownloaderApp (QWidget ):
|
|||||||
'unwanted_keywords':{'spicy','hd','nsfw','4k','preview','teaser','clip'},
|
'unwanted_keywords':{'spicy','hd','nsfw','4k','preview','teaser','clip'},
|
||||||
'filter_mode':self .get_filter_mode (),
|
'filter_mode':self .get_filter_mode (),
|
||||||
'skip_zip':self .skip_zip_checkbox .isChecked (),
|
'skip_zip':self .skip_zip_checkbox .isChecked (),
|
||||||
'skip_rar':self .skip_rar_checkbox .isChecked (),
|
|
||||||
'use_subfolders':self .use_subfolders_checkbox .isChecked (),
|
'use_subfolders':self .use_subfolders_checkbox .isChecked (),
|
||||||
'use_post_subfolders':self .use_subfolder_per_post_checkbox .isChecked (),
|
'use_post_subfolders':self .use_subfolder_per_post_checkbox .isChecked (),
|
||||||
'compress_images':self .compress_images_checkbox .isChecked (),
|
'compress_images':self .compress_images_checkbox .isChecked (),
|
||||||
@@ -4276,6 +4391,23 @@ class DownloaderApp (QWidget ):
|
|||||||
self .retry_thread_pool .shutdown (wait =True )
|
self .retry_thread_pool .shutdown (wait =True )
|
||||||
self .retry_thread_pool =None
|
self .retry_thread_pool =None
|
||||||
|
|
||||||
|
if os.path.exists(self.session_file_path):
|
||||||
|
try:
|
||||||
|
with self.session_lock:
|
||||||
|
# Read the current session data
|
||||||
|
with open(self.session_file_path, 'r', encoding='utf-8') as f:
|
||||||
|
session_data = json.load(f)
|
||||||
|
|
||||||
|
if 'download_state' in session_data:
|
||||||
|
session_data['download_state']['permanently_failed_files'] = self.permanently_failed_files_for_dialog
|
||||||
|
|
||||||
|
# Save the updated session data back to the file
|
||||||
|
self._save_session_file(session_data)
|
||||||
|
self.log_signal.emit("ℹ️ Session file updated with retry results.")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.log_signal.emit(f"⚠️ Could not update session file after retry: {e}")
|
||||||
|
|
||||||
if self .external_link_download_thread and not self .external_link_download_thread .isRunning ():
|
if self .external_link_download_thread and not self .external_link_download_thread .isRunning ():
|
||||||
self .external_link_download_thread .deleteLater ()
|
self .external_link_download_thread .deleteLater ()
|
||||||
self .external_link_download_thread =None
|
self .external_link_download_thread =None
|
||||||
@@ -4454,7 +4586,6 @@ class DownloaderApp (QWidget ):
|
|||||||
# Set radio buttons and checkboxes to defaults
|
# Set radio buttons and checkboxes to defaults
|
||||||
self.radio_all.setChecked(True)
|
self.radio_all.setChecked(True)
|
||||||
self.skip_zip_checkbox.setChecked(True)
|
self.skip_zip_checkbox.setChecked(True)
|
||||||
self.skip_rar_checkbox.setChecked(True)
|
|
||||||
self.download_thumbnails_checkbox.setChecked(False)
|
self.download_thumbnails_checkbox.setChecked(False)
|
||||||
self.compress_images_checkbox.setChecked(False)
|
self.compress_images_checkbox.setChecked(False)
|
||||||
self.use_subfolders_checkbox.setChecked(True)
|
self.use_subfolders_checkbox.setChecked(True)
|
||||||
@@ -4781,13 +4912,9 @@ class DownloaderApp (QWidget ):
|
|||||||
self.favorite_download_queue.append(queue_item)
|
self.favorite_download_queue.append(queue_item)
|
||||||
|
|
||||||
if self.favorite_download_queue:
|
if self.favorite_download_queue:
|
||||||
# --- NEW: This block adds the selected creator names to the input field ---
|
|
||||||
if hasattr(self, 'link_input'):
|
if hasattr(self, 'link_input'):
|
||||||
# 1. Get all the names from the queue
|
|
||||||
creator_names = [item['name'] for item in self.favorite_download_queue]
|
creator_names = [item['name'] for item in self.favorite_download_queue]
|
||||||
# 2. Join them into a single string
|
|
||||||
display_text = ", ".join(creator_names)
|
display_text = ", ".join(creator_names)
|
||||||
# 3. Set the text of the URL input field
|
|
||||||
self.link_input.setText(display_text)
|
self.link_input.setText(display_text)
|
||||||
|
|
||||||
self.log_signal.emit(f"ℹ️ {len(self.favorite_download_queue)} creators added to download queue from popup. Click 'Start Download' to process.")
|
self.log_signal.emit(f"ℹ️ {len(self.favorite_download_queue)} creators added to download queue from popup. Click 'Start Download' to process.")
|
||||||
@@ -4982,7 +5109,7 @@ class DownloaderApp (QWidget ):
|
|||||||
override_dir =os .path .normpath (os .path .join (main_download_dir ,item_specific_folder_name ))
|
override_dir =os .path .normpath (os .path .join (main_download_dir ,item_specific_folder_name ))
|
||||||
self .log_signal .emit (f" Scope requires artist folder. Target directory: '{override_dir }'")
|
self .log_signal .emit (f" Scope requires artist folder. Target directory: '{override_dir }'")
|
||||||
|
|
||||||
success_starting_download =self .start_download (direct_api_url =next_url ,override_output_dir =override_dir )
|
success_starting_download =self .start_download (direct_api_url =next_url ,override_output_dir =override_dir, is_continuation=True )
|
||||||
|
|
||||||
if not success_starting_download :
|
if not success_starting_download :
|
||||||
self .log_signal .emit (f"⚠️ Failed to initiate download for '{item_display_name }'. Skipping this item in queue.")
|
self .log_signal .emit (f"⚠️ Failed to initiate download for '{item_display_name }'. Skipping this item in queue.")
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ MAX_FILENAME_COMPONENT_LENGTH = 150
|
|||||||
|
|
||||||
# Sets of file extensions for quick type checking
|
# Sets of file extensions for quick type checking
|
||||||
IMAGE_EXTENSIONS = {
|
IMAGE_EXTENSIONS = {
|
||||||
'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff', '.tif', '.webp',
|
'.jpg', '.jpeg', '.jpe', '.png', '.gif', '.bmp', '.tiff', '.tif', '.webp',
|
||||||
'.heic', '.heif', '.svg', '.ico', '.jfif', '.pjpeg', '.pjp', '.avif'
|
'.heic', '.heif', '.svg', '.ico', '.jfif', '.pjpeg', '.pjp', '.avif'
|
||||||
}
|
}
|
||||||
VIDEO_EXTENSIONS = {
|
VIDEO_EXTENSIONS = {
|
||||||
|
|||||||
@@ -24,19 +24,14 @@ def setup_ui(main_app):
|
|||||||
Args:
|
Args:
|
||||||
main_app: The instance of the main DownloaderApp.
|
main_app: The instance of the main DownloaderApp.
|
||||||
"""
|
"""
|
||||||
# --- START: Modified Scaling Logic ---
|
|
||||||
# Force a fixed scale factor to disable UI scaling on high-DPI screens.
|
|
||||||
scale = float(main_app.settings.value(UI_SCALE_KEY, 1.0))
|
scale = float(main_app.settings.value(UI_SCALE_KEY, 1.0))
|
||||||
main_app.scale_factor = scale
|
main_app.scale_factor = scale
|
||||||
|
|
||||||
# --- Set the global font size for the application ---
|
|
||||||
default_font = QApplication.font()
|
default_font = QApplication.font()
|
||||||
base_font_size = 9 # Use a standard base size
|
base_font_size = 9 # Use a standard base size
|
||||||
default_font.setPointSize(int(base_font_size * scale))
|
default_font.setPointSize(int(base_font_size * scale))
|
||||||
main_app.setFont(default_font)
|
main_app.setFont(default_font)
|
||||||
# --- END: Modified Scaling Logic ---
|
|
||||||
|
|
||||||
# --- Set the global font size for the application ---
|
|
||||||
default_font = QApplication.font()
|
default_font = QApplication.font()
|
||||||
base_font_size = 9 # Use a standard base size
|
base_font_size = 9 # Use a standard base size
|
||||||
default_font.setPointSize(int(base_font_size * scale))
|
default_font.setPointSize(int(base_font_size * scale))
|
||||||
@@ -221,12 +216,10 @@ def setup_ui(main_app):
|
|||||||
checkboxes_group_layout.setSpacing(10)
|
checkboxes_group_layout.setSpacing(10)
|
||||||
row1_layout = QHBoxLayout()
|
row1_layout = QHBoxLayout()
|
||||||
row1_layout.setSpacing(10)
|
row1_layout.setSpacing(10)
|
||||||
main_app.skip_zip_checkbox = QCheckBox("Skip .zip")
|
main_app.skip_zip_checkbox = QCheckBox("Skip archives")
|
||||||
|
main_app.skip_zip_checkbox.setToolTip("Skip Common Archives (Eg.. Zip, Rar, 7z)")
|
||||||
main_app.skip_zip_checkbox.setChecked(True)
|
main_app.skip_zip_checkbox.setChecked(True)
|
||||||
row1_layout.addWidget(main_app.skip_zip_checkbox)
|
row1_layout.addWidget(main_app.skip_zip_checkbox)
|
||||||
main_app.skip_rar_checkbox = QCheckBox("Skip .rar")
|
|
||||||
main_app.skip_rar_checkbox.setChecked(True)
|
|
||||||
row1_layout.addWidget(main_app.skip_rar_checkbox)
|
|
||||||
main_app.download_thumbnails_checkbox = QCheckBox("Download Thumbnails Only")
|
main_app.download_thumbnails_checkbox = QCheckBox("Download Thumbnails Only")
|
||||||
row1_layout.addWidget(main_app.download_thumbnails_checkbox)
|
row1_layout.addWidget(main_app.download_thumbnails_checkbox)
|
||||||
main_app.scan_content_images_checkbox = QCheckBox("Scan Content for Images")
|
main_app.scan_content_images_checkbox = QCheckBox("Scan Content for Images")
|
||||||
@@ -246,7 +239,7 @@ def setup_ui(main_app):
|
|||||||
checkboxes_group_layout.addWidget(advanced_settings_label)
|
checkboxes_group_layout.addWidget(advanced_settings_label)
|
||||||
advanced_row1_layout = QHBoxLayout()
|
advanced_row1_layout = QHBoxLayout()
|
||||||
advanced_row1_layout.setSpacing(10)
|
advanced_row1_layout.setSpacing(10)
|
||||||
main_app.use_subfolders_checkbox = QCheckBox("Separate Folders by Name/Title")
|
main_app.use_subfolders_checkbox = QCheckBox("Separate Folders by Known.txt")
|
||||||
main_app.use_subfolders_checkbox.setChecked(True)
|
main_app.use_subfolders_checkbox.setChecked(True)
|
||||||
main_app.use_subfolders_checkbox.toggled.connect(main_app.update_ui_for_subfolders)
|
main_app.use_subfolders_checkbox.toggled.connect(main_app.update_ui_for_subfolders)
|
||||||
advanced_row1_layout.addWidget(main_app.use_subfolders_checkbox)
|
advanced_row1_layout.addWidget(main_app.use_subfolders_checkbox)
|
||||||
|
|||||||
Reference in New Issue
Block a user