diff --git a/src/core/api_client.py b/src/core/api_client.py index 2d56524..e4a7490 100644 --- a/src/core/api_client.py +++ b/src/core/api_client.py @@ -3,8 +3,6 @@ import traceback from urllib.parse import urlparse import json # Ensure json is imported import requests - -# (Keep the rest of your imports) from ..utils.network_utils import extract_post_info, prepare_cookies_for_request from ..config.constants import ( STYLE_DATE_POST_TITLE @@ -25,9 +23,6 @@ def fetch_posts_paginated(api_url_base, headers, offset, logger, cancellation_ev raise RuntimeError("Fetch operation cancelled by user while paused.") time.sleep(0.5) logger(" Post fetching resumed.") - - # --- MODIFICATION: Added `fields` to the URL to request only metadata --- - # This prevents the large 'content' field from being included in the list, avoiding timeouts. fields_to_request = "id,user,service,title,shared_file,added,published,edited,file,attachments,tags" paginated_url = f'{api_url_base}?o={offset}&fields={fields_to_request}' @@ -44,7 +39,6 @@ def fetch_posts_paginated(api_url_base, headers, offset, logger, cancellation_ev logger(log_message) try: - # We can now remove the streaming logic as the response will be small and fast. response = requests.get(paginated_url, headers=headers, timeout=(15, 60), cookies=cookies_dict) response.raise_for_status() return response.json() @@ -80,7 +74,6 @@ def fetch_single_post_data(api_domain, service, user_id, post_id, headers, logge post_api_url = f"https://{api_domain}/api/v1/{service}/user/{user_id}/post/{post_id}" logger(f" Fetching full content for post ID {post_id}...") try: - # Use streaming here as a precaution for single posts that are still very large. with requests.get(post_api_url, headers=headers, timeout=(15, 300), cookies=cookies_dict, stream=True) as response: response.raise_for_status() response_body = b"" @@ -88,7 +81,6 @@ def fetch_single_post_data(api_domain, service, user_id, post_id, headers, logge response_body += chunk full_post_data = json.loads(response_body) - # The API sometimes wraps the post in a list, handle that. if isinstance(full_post_data, list) and full_post_data: return full_post_data[0] return full_post_data @@ -134,14 +126,10 @@ def download_from_api( 'User-Agent': 'Mozilla/5.0', 'Accept': 'application/json' } - - # --- ADD THIS BLOCK --- - # Ensure processed_post_ids is a set for fast lookups if processed_post_ids is None: processed_post_ids = set() else: processed_post_ids = set(processed_post_ids) - # --- END OF ADDITION --- service, user_id, target_post_id = extract_post_info(api_url_input) @@ -158,11 +146,9 @@ def download_from_api( if use_cookie and app_base_dir: cookies_for_api = prepare_cookies_for_request(use_cookie, cookie_text, selected_cookie_file, app_base_dir, logger, target_domain=api_domain) if target_post_id: - # --- ADD THIS CHECK FOR RESTORE --- if target_post_id in processed_post_ids: logger(f" Skipping already processed target post ID: {target_post_id}") return - # --- END OF ADDITION --- direct_post_api_url = f"https://{api_domain}/api/v1/{service}/user/{user_id}/post/{target_post_id}" logger(f" Attempting direct fetch for target post: {direct_post_api_url}") try: @@ -248,14 +234,12 @@ def download_from_api( break if cancellation_event and cancellation_event.is_set(): return if all_posts_for_manga_mode: - # --- ADD THIS BLOCK TO FILTER POSTS IN MANGA MODE --- if processed_post_ids: original_count = len(all_posts_for_manga_mode) all_posts_for_manga_mode = [post for post in all_posts_for_manga_mode if post.get('id') not in processed_post_ids] skipped_count = original_count - len(all_posts_for_manga_mode) if skipped_count > 0: logger(f" Manga Mode: Skipped {skipped_count} already processed post(s) before sorting.") - # --- END OF ADDITION --- logger(f" Manga Mode: Fetched {len(all_posts_for_manga_mode)} total posts. Sorting by publication date (oldest first)...") def sort_key_tuple(post): @@ -326,15 +310,12 @@ def download_from_api( logger(f"❌ Unexpected error fetching page {current_page_num} (offset {current_offset}): {e}") traceback.print_exc() break - - # --- ADD THIS BLOCK TO FILTER POSTS IN STANDARD MODE --- if processed_post_ids: original_count = len(posts_batch) posts_batch = [post for post in posts_batch if post.get('id') not in processed_post_ids] skipped_count = original_count - len(posts_batch) if skipped_count > 0: logger(f" Skipped {skipped_count} already processed post(s) from page {current_page_num}.") - # --- END OF ADDITION --- if not posts_batch: if target_post_id and not processed_target_post_flag: diff --git a/src/core/manager.py b/src/core/manager.py index 96e5170..a4f02cb 100644 --- a/src/core/manager.py +++ b/src/core/manager.py @@ -1,13 +1,9 @@ -# --- Standard Library Imports --- import threading import time import os import json import traceback from concurrent.futures import ThreadPoolExecutor, as_completed, Future - -# --- Local Application Imports --- -# These imports reflect the new, organized project structure. from .api_client import download_from_api from .workers import PostProcessorWorker, DownloadThread from ..config.constants import ( @@ -36,8 +32,6 @@ class DownloadManager: self.progress_queue = progress_queue self.thread_pool = None self.active_futures = [] - - # --- Session State --- self.cancellation_event = threading.Event() self.pause_event = threading.Event() self.is_running = False @@ -64,8 +58,6 @@ class DownloadManager: if self.is_running: self._log("❌ Cannot start a new session: A session is already in progress.") return - - # --- Reset state for the new session --- self.is_running = True self.cancellation_event.clear() self.pause_event.clear() @@ -75,8 +67,6 @@ class DownloadManager: self.total_downloads = 0 self.total_skips = 0 self.all_kept_original_filenames = [] - - # --- Decide execution strategy (multi-threaded vs. single-threaded) --- is_single_post = bool(config.get('target_post_id_from_initial_url')) use_multithreading = config.get('use_multithreading', True) is_manga_sequential = config.get('manga_mode_active') and config.get('manga_filename_style') in [STYLE_DATE_BASED, STYLE_POST_TITLE_GLOBAL_NUMBERING] @@ -84,7 +74,6 @@ class DownloadManager: should_use_multithreading_for_posts = use_multithreading and not is_single_post and not is_manga_sequential if should_use_multithreading_for_posts: - # Start a separate thread to manage fetching and queuing to the thread pool fetcher_thread = threading.Thread( target=self._fetch_and_queue_posts_for_pool, args=(config, restore_data), @@ -92,16 +81,11 @@ class DownloadManager: ) fetcher_thread.start() else: - # For single posts or sequential manga mode, use a single worker thread - # which is simpler and ensures order. self._start_single_threaded_session(config) def _start_single_threaded_session(self, config): """Handles downloads that are best processed by a single worker thread.""" self._log("ℹ️ Initializing single-threaded download process...") - - # The original DownloadThread is now a pure Python thread, not a QThread. - # We run its `run` method in a standard Python thread. self.worker_thread = threading.Thread( target=self._run_single_worker, args=(config,), @@ -112,7 +96,6 @@ class DownloadManager: def _run_single_worker(self, config): """Target function for the single-worker thread.""" try: - # Pass the queue directly to the worker for it to send updates worker = DownloadThread(config, self.progress_queue) worker.run() # This is the main blocking call for this thread except Exception as e: @@ -129,9 +112,6 @@ class DownloadManager: try: num_workers = min(config.get('num_threads', 4), MAX_THREADS) self.thread_pool = ThreadPoolExecutor(max_workers=num_workers, thread_name_prefix='PostWorker_') - - # Fetch posts - # In a real implementation, this would call `api_client.download_from_api` if restore_data: all_posts = restore_data['all_posts_data'] processed_ids = set(restore_data['processed_post_ids']) @@ -149,12 +129,9 @@ class DownloadManager: if not posts_to_process: self._log("✅ No new posts to process.") return - - # Submit tasks to the pool for post_data in posts_to_process: if self.cancellation_event.is_set(): break - # Each PostProcessorWorker gets the queue to send its own updates worker = PostProcessorWorker(post_data, config, self.progress_queue) future = self.thread_pool.submit(worker.process) future.add_done_callback(self._handle_future_result) @@ -164,27 +141,32 @@ class DownloadManager: self._log(f"❌ CRITICAL ERROR in post fetcher thread: {e}") self._log(traceback.format_exc()) finally: - # Wait for all submitted tasks to complete before shutting down if self.thread_pool: self.thread_pool.shutdown(wait=True) self.is_running = False self._log("🏁 All processing tasks have completed.") - # Emit final signal self.progress_queue.put({ 'type': 'finished', 'payload': (self.total_downloads, self.total_skips, self.cancellation_event.is_set(), self.all_kept_original_filenames) }) - + def _get_all_posts(self, config): """Helper to fetch all posts using the API client.""" all_posts = [] - # This generator yields batches of posts post_generator = download_from_api( api_url_input=config['api_url'], logger=self._log, - # ... pass other relevant config keys ... + start_page=config.get('start_page'), + end_page=config.get('end_page'), + manga_mode=config.get('manga_mode_active', False), cancellation_event=self.cancellation_event, - pause_event=self.pause_event + pause_event=self.pause_event, + use_cookie=config.get('use_cookie', False), + cookie_text=config.get('cookie_text', ''), + selected_cookie_file=config.get('selected_cookie_file'), + app_base_dir=config.get('app_base_dir'), + manga_filename_style_for_sort_check=config.get('manga_filename_style'), + processed_post_ids=config.get('processed_post_ids', []) ) for batch in post_generator: all_posts.extend(batch) @@ -203,14 +185,11 @@ class DownloadManager: self.total_skips += 1 else: result = future.result() - # Unpack result tuple from the worker (dl_count, skip_count, kept_originals, retryable, permanent, history) = result self.total_downloads += dl_count self.total_skips += skip_count self.all_kept_original_filenames.extend(kept_originals) - - # Queue up results for UI to handle if retryable: self.progress_queue.put({'type': 'retryable_failure', 'payload': (retryable,)}) if permanent: @@ -221,8 +200,6 @@ class DownloadManager: except Exception as e: self._log(f"❌ Worker task resulted in an exception: {e}") self.total_skips += 1 # Count errored posts as skipped - - # Update overall progress self.progress_queue.put({'type': 'overall_progress', 'payload': (self.total_posts, self.processed_posts)}) def cancel_session(self): @@ -231,11 +208,7 @@ class DownloadManager: return self._log("⚠️ Cancellation requested by user...") self.cancellation_event.set() - - # For single thread mode, the worker checks the event - # For multi-thread mode, shut down the pool if self.thread_pool: - # Don't wait, just cancel pending futures and let the fetcher thread exit self.thread_pool.shutdown(wait=False, cancel_futures=True) self.is_running = False diff --git a/src/core/workers.py b/src/core/workers.py index eb6c907..1e2d182 100644 --- a/src/core/workers.py +++ b/src/core/workers.py @@ -1,4 +1,3 @@ -# --- Standard Library Imports --- import os import queue import re @@ -15,15 +14,12 @@ from concurrent.futures import ThreadPoolExecutor, as_completed, CancelledError, from io import BytesIO from urllib .parse import urlparse import requests -# --- Third-Party Library Imports --- try: from PIL import Image except ImportError: Image = None -# try: from fpdf import FPDF - # Add a simple class to handle the header/footer for stories class PDF(FPDF): def header(self): pass # No header @@ -39,16 +35,12 @@ try: from docx import Document except ImportError: Document = None - -# --- PyQt5 Imports --- from PyQt5 .QtCore import Qt ,QThread ,pyqtSignal ,QMutex ,QMutexLocker ,QObject ,QTimer ,QSettings ,QStandardPaths ,QCoreApplication ,QUrl ,QSize ,QProcess -# --- Local Application Imports --- from .api_client import download_from_api, fetch_post_comments from ..services.multipart_downloader import download_file_in_parts, MULTIPART_DOWNLOADER_AVAILABLE from ..services.drive_downloader import ( download_mega_file, download_gdrive_file, download_dropbox_file ) -# Corrected Imports: from ..utils.file_utils import ( is_image, is_video, is_zip, is_rar, is_archive, is_audio, KNOWN_NAMES, clean_filename, clean_folder_name @@ -567,10 +559,8 @@ class PostProcessorWorker: with self.downloaded_hash_counts_lock: current_count = self.downloaded_hash_counts.get(calculated_file_hash, 0) - # Default to not skipping decision_to_skip = False - # Apply logic based on mode if self.keep_duplicates_mode == DUPLICATE_HANDLING_HASH: if current_count >= 1: decision_to_skip = True @@ -581,12 +571,10 @@ class PostProcessorWorker: decision_to_skip = True self.logger(f" -> Skip (Duplicate Limit Reached): Limit of {self.keep_duplicates_limit} for this file content has been met. Discarding.") - # If we are NOT skipping this file, we MUST increment the count. if not decision_to_skip: self.downloaded_hash_counts[calculated_file_hash] = current_count + 1 should_skip = decision_to_skip - # --- End of Final Corrected Logic --- if should_skip: if downloaded_part_file_path and os.path.exists(downloaded_part_file_path): @@ -678,9 +666,14 @@ class PostProcessorWorker: else: self.logger(f"->>Download Fail for '{api_original_filename}' (Post ID: {original_post_id_for_log}). No successful download after retries.") details_for_failure = { - 'file_info': file_info, 'target_folder_path': target_folder_path, 'headers': headers, - 'original_post_id_for_log': original_post_id_for_log, 'post_title': post_title, - 'file_index_in_post': file_index_in_post, 'num_files_in_this_post': num_files_in_this_post + 'file_info': file_info, + 'target_folder_path': target_folder_path, + 'headers': headers, + 'original_post_id_for_log': original_post_id_for_log, + 'post_title': post_title, + 'file_index_in_post': file_index_in_post, + 'num_files_in_this_post': num_files_in_this_post, + 'forced_filename_override': filename_to_save_in_main_path } if is_permanent_error: return 0, 1, filename_to_save_in_main_path, was_original_name_kept_flag, FILE_DOWNLOAD_STATUS_FAILED_PERMANENTLY_THIS_SESSION, details_for_failure @@ -1040,7 +1033,9 @@ class PostProcessorWorker: return result_tuple raw_text_content = "" + comments_data = [] final_post_data = post_data + if self.text_only_scope == 'content' and 'content' not in final_post_data: self.logger(f" Post {post_id} is missing 'content' field, fetching full data...") parsed_url = urlparse(self.api_url_input) @@ -1050,6 +1045,7 @@ class PostProcessorWorker: full_data = fetch_single_post_data(api_domain, self.service, self.user_id, post_id, headers, self.logger, cookies_dict=cookies) if full_data: final_post_data = full_data + if self.text_only_scope == 'content': raw_text_content = final_post_data.get('content', '') elif self.text_only_scope == 'comments': @@ -1060,46 +1056,46 @@ class PostProcessorWorker: if comments_data: comment_texts = [] for comment in comments_data: - user = comment.get('user', {}).get('name', 'Unknown User') - timestamp = comment.get('updated', 'No Date') + user = comment.get('commenter_name', 'Unknown User') + timestamp = comment.get('published', 'No Date') body = strip_html_tags(comment.get('content', '')) comment_texts.append(f"--- Comment by {user} on {timestamp} ---\n{body}\n") raw_text_content = "\n".join(comment_texts) + else: + raw_text_content = "" except Exception as e: self.logger(f" ❌ Error fetching comments for text-only mode: {e}") - if not raw_text_content or not raw_text_content.strip(): + cleaned_text = "" + if self.text_only_scope == 'content': + if not raw_text_content: + cleaned_text = "" + else: + text_with_newlines = re.sub(r'(?i)
| tags found. Falling back to basic HTML cleaning for the whole block.")
- text_with_br = re.sub(r'
', '\n', raw_text_content, flags=re.IGNORECASE)
- cleaned_text = re.sub(r'<.*?>', '', text_with_br)
- else:
- cleaned_paragraphs_list = []
- for p_content in html_paragraphs:
- p_with_br = re.sub(r'
', '\n', p_content, flags=re.IGNORECASE)
- p_cleaned = re.sub(r'<.*?>', '', p_with_br)
- p_final = html.unescape(p_cleaned).strip()
- if p_final:
- cleaned_paragraphs_list.append(p_final)
- cleaned_text = '\n\n'.join(cleaned_paragraphs_list)
- cleaned_text = cleaned_text.replace('…', '...')
-
if self.single_pdf_mode:
- if not cleaned_text:
- result_tuple = (0, 0, [], [], [], None, None)
- return result_tuple
content_data = {
'title': post_title,
- 'content': cleaned_text,
'published': self.post.get('published') or self.post.get('added')
}
+ if self.text_only_scope == 'comments':
+ if not comments_data: return (0, 0, [], [], [], None, None)
+ content_data['comments'] = comments_data
+ else:
+ if not cleaned_text.strip(): return (0, 0, [], [], [], None, None)
+ content_data['content'] = cleaned_text
+
temp_dir = os.path.join(self.app_base_dir, "appdata")
os.makedirs(temp_dir, exist_ok=True)
temp_filename = f"tmp_{post_id}_{uuid.uuid4().hex[:8]}.json"
@@ -1107,13 +1103,11 @@ class PostProcessorWorker:
try:
with open(temp_filepath, 'w', encoding='utf-8') as f:
json.dump(content_data, f, indent=2)
- self.logger(f" Saved temporary text for '{post_title}' for single PDF compilation.")
- result_tuple = (0, 0, [], [], [], None, temp_filepath)
- return result_tuple
+ self.logger(f" Saved temporary data for '{post_title}' for single PDF compilation.")
+ return (0, 0, [], [], [], None, temp_filepath)
except Exception as e:
self.logger(f" ❌ Failed to write temporary file for single PDF: {e}")
- result_tuple = (0, 0, [], [], [], None, None)
- return result_tuple
+ return (0, 0, [], [], [], None, None)
else:
file_extension = self.text_export_format
txt_filename = clean_filename(post_title) + f".{file_extension}"
@@ -1125,27 +1119,63 @@ class PostProcessorWorker:
while os.path.exists(final_save_path):
final_save_path = f"{base}_{counter}{ext}"
counter += 1
+
if file_extension == 'pdf':
if FPDF:
- self.logger(f" Converting to PDF...")
+ self.logger(f" Creating formatted PDF for {'comments' if self.text_only_scope == 'comments' else 'content'}...")
pdf = PDF()
font_path = ""
+ bold_font_path = ""
if self.project_root_dir:
font_path = os.path.join(self.project_root_dir, 'data', 'dejavu-sans', 'DejaVuSans.ttf')
+ bold_font_path = os.path.join(self.project_root_dir, 'data', 'dejavu-sans', 'DejaVuSans-Bold.ttf')
+
try:
if not os.path.exists(font_path): raise RuntimeError(f"Font file not found: {font_path}")
+ if not os.path.exists(bold_font_path): raise RuntimeError(f"Bold font file not found: {bold_font_path}")
pdf.add_font('DejaVu', '', font_path, uni=True)
- pdf.set_font('DejaVu', '', 12)
+ pdf.add_font('DejaVu', 'B', bold_font_path, uni=True)
+ default_font_family = 'DejaVu'
except Exception as font_error:
self.logger(f" ⚠️ Could not load DejaVu font: {font_error}. Falling back to Arial.")
- pdf.set_font('Arial', '', 12)
+ default_font_family = 'Arial'
+
pdf.add_page()
- pdf.multi_cell(0, 5, cleaned_text)
+ pdf.set_font(default_font_family, 'B', 16)
+ pdf.multi_cell(0, 10, post_title)
+ pdf.ln(10)
+
+ if self.text_only_scope == 'comments':
+ if not comments_data:
+ self.logger(" -> Skip PDF Creation: No comments to process.")
+ return (0, num_potential_files_in_post, [], [], [], None, None)
+ for i, comment in enumerate(comments_data):
+ user = comment.get('commenter_name', 'Unknown User')
+ timestamp = comment.get('published', 'No Date')
+ body = strip_html_tags(comment.get('content', ''))
+ pdf.set_font(default_font_family, '', 10)
+ pdf.write(8, "Comment by: ")
+ pdf.set_font(default_font_family, 'B', 10)
+ pdf.write(8, user)
+ pdf.set_font(default_font_family, '', 10)
+ pdf.write(8, f" on {timestamp}")
+ pdf.ln(10)
+ pdf.set_font(default_font_family, '', 11)
+ pdf.multi_cell(0, 7, body)
+ if i < len(comments_data) - 1:
+ pdf.ln(5)
+ pdf.cell(0, 0, '', border='T')
+ pdf.ln(5)
+ else:
+ pdf.set_font(default_font_family, '', 12)
+ pdf.multi_cell(0, 7, cleaned_text)
+
pdf.output(final_save_path)
else:
self.logger(f" ⚠️ Cannot create PDF: 'fpdf2' library not installed. Saving as .txt.")
final_save_path = os.path.splitext(final_save_path)[0] + ".txt"
with open(final_save_path, 'w', encoding='utf-8') as f: f.write(cleaned_text)
+
elif file_extension == 'docx':
if Document:
self.logger(f" Converting to DOCX...")
@@ -1156,12 +1186,15 @@ class PostProcessorWorker:
self.logger(f" ⚠️ Cannot create DOCX: 'python-docx' library not installed. Saving as .txt.")
final_save_path = os.path.splitext(final_save_path)[0] + ".txt"
with open(final_save_path, 'w', encoding='utf-8') as f: f.write(cleaned_text)
- else:
+
+ else: # TXT file
with open(final_save_path, 'w', encoding='utf-8') as f:
f.write(cleaned_text)
+
self.logger(f"✅ Saved Text: '{os.path.basename(final_save_path)}' in '{os.path.basename(determined_post_save_path_for_history)}'")
result_tuple = (1, num_potential_files_in_post, [], [], [], history_data_for_this_post, None)
return result_tuple
+
except Exception as e:
self.logger(f" ❌ Critical error saving text file '{txt_filename}': {e}")
result_tuple = (0, num_potential_files_in_post, [], [], [], None, None)
@@ -1263,7 +1296,6 @@ class PostProcessorWorker:
if self.keep_duplicates_mode == DUPLICATE_HANDLING_HASH:
unique_files_by_url = {}
for file_info in all_files_from_post_api:
- # Use the file URL as a unique key to avoid processing the same file multiple times
file_url = file_info.get('url')
if file_url and file_url not in unique_files_by_url:
unique_files_by_url[file_url] = file_info
@@ -1734,7 +1766,6 @@ class DownloadThread(QThread):
worker_signals_obj = PostProcessorSignals()
try:
- # Connect signals
worker_signals_obj.progress_signal.connect(self.progress_signal)
worker_signals_obj.file_download_status_signal.connect(self.file_download_status_signal)
worker_signals_obj.file_progress_signal.connect(self.file_progress_signal)
@@ -1771,8 +1802,6 @@ class DownloadThread(QThread):
was_process_cancelled = True
break
- # --- START OF FIX: Explicitly build the arguments dictionary ---
- # This robustly maps all thread attributes to the correct worker parameters.
worker_args = {
'post_data': individual_post_data,
'emitter': worker_signals_obj,
@@ -1833,7 +1862,6 @@ class DownloadThread(QThread):
'single_pdf_mode': self.single_pdf_mode,
'project_root_dir': self.project_root_dir,
}
- # --- END OF FIX ---
post_processing_worker = PostProcessorWorker(**worker_args)
@@ -1860,6 +1888,7 @@ class DownloadThread(QThread):
if not was_process_cancelled and not self.isInterruptionRequested():
self.logger("✅ All posts processed or end of content reached by DownloadThread.")
+
except Exception as main_thread_err:
self.logger(f"\n❌ Critical error within DownloadThread run loop: {main_thread_err}")
traceback.print_exc()
diff --git a/src/ui/assets.py b/src/ui/assets.py
index ac3ec13..fe2ec76 100644
--- a/src/ui/assets.py
+++ b/src/ui/assets.py
@@ -1,8 +1,5 @@
-# --- Standard Library Imports ---
import os
import sys
-
-# --- PyQt5 Imports ---
from PyQt5.QtGui import QIcon
_app_icon_cache = None
diff --git a/src/ui/dialogs/ConfirmAddAllDialog.py b/src/ui/dialogs/ConfirmAddAllDialog.py
index be91480..8994e94 100644
--- a/src/ui/dialogs/ConfirmAddAllDialog.py
+++ b/src/ui/dialogs/ConfirmAddAllDialog.py
@@ -1,18 +1,10 @@
-# --- PyQt5 Imports ---
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
QApplication, QDialog, QHBoxLayout, QLabel, QListWidget, QListWidgetItem,
QPushButton, QVBoxLayout
)
-
-# --- Local Application Imports ---
-# This assumes the new project structure is in place.
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
-
-# --- Constants for Dialog Choices ---
-# These were moved from main.py to be self-contained within this module's context.
CONFIRM_ADD_ALL_ACCEPTED = 1
CONFIRM_ADD_ALL_SKIP_ADDING = 2
CONFIRM_ADD_ALL_CANCEL_DOWNLOAD = 3
@@ -38,23 +30,16 @@ class ConfirmAddAllDialog(QDialog):
self.parent_app = parent_app
self.setModal(True)
self.new_filter_objects_list = new_filter_objects_list
- # Default choice if the dialog is closed without a button press
self.user_choice = CONFIRM_ADD_ALL_CANCEL_DOWNLOAD
-
- # --- Basic Window Setup ---
app_icon = get_app_icon_object()
if app_icon and not app_icon.isNull():
self.setWindowIcon(app_icon)
-
- # Set window size dynamically
screen_height = QApplication.primaryScreen().availableGeometry().height() if QApplication.primaryScreen() else 768
scale_factor = screen_height / 768.0
base_min_w, base_min_h = 480, 350
scaled_min_w = int(base_min_w * scale_factor)
scaled_min_h = int(base_min_h * scale_factor)
self.setMinimumSize(scaled_min_w, scaled_min_h)
-
- # --- Initialize UI and Apply Theming ---
self._init_ui()
self._retranslate_ui()
self._apply_theme()
@@ -70,8 +55,6 @@ class ConfirmAddAllDialog(QDialog):
self.names_list_widget = QListWidget()
self._populate_list()
main_layout.addWidget(self.names_list_widget)
-
- # --- Selection Buttons ---
selection_buttons_layout = QHBoxLayout()
self.select_all_button = QPushButton()
self.select_all_button.clicked.connect(self._select_all_items)
@@ -82,8 +65,6 @@ class ConfirmAddAllDialog(QDialog):
selection_buttons_layout.addWidget(self.deselect_all_button)
selection_buttons_layout.addStretch()
main_layout.addLayout(selection_buttons_layout)
-
- # --- Action Buttons ---
buttons_layout = QHBoxLayout()
self.add_selected_button = QPushButton()
self.add_selected_button.clicked.connect(self._accept_add_selected)
@@ -171,7 +152,6 @@ class ConfirmAddAllDialog(QDialog):
sensible default if no items are selected but the "Add" button is clicked.
"""
super().exec_()
- # If the user clicked "Add Selected" but didn't select any items, treat it as skipping.
if isinstance(self.user_choice, list) and not self.user_choice:
return CONFIRM_ADD_ALL_SKIP_ADDING
return self.user_choice
diff --git a/src/ui/dialogs/DownloadExtractedLinksDialog.py b/src/ui/dialogs/DownloadExtractedLinksDialog.py
index b886659..bcc2a69 100644
--- a/src/ui/dialogs/DownloadExtractedLinksDialog.py
+++ b/src/ui/dialogs/DownloadExtractedLinksDialog.py
@@ -1,14 +1,9 @@
-# --- Standard Library Imports ---
from collections import defaultdict
-
-# --- PyQt5 Imports ---
from PyQt5.QtCore import pyqtSignal, Qt
from PyQt5.QtWidgets import (
QApplication, QDialog, QHBoxLayout, QLabel, QListWidget, QListWidgetItem,
QMessageBox, QPushButton, QVBoxLayout, QAbstractItemView
)
-
-# --- Local Application Imports ---
from ...i18n.translator import get_translation
from ..main_window import get_app_icon_object
from ...utils.resolution import get_dark_theme
@@ -18,8 +13,6 @@ class DownloadExtractedLinksDialog(QDialog):
A dialog to select and initiate the download for extracted, supported links
from external cloud services like Mega, Google Drive, and Dropbox.
"""
-
- # Signal emitted with a list of selected link information dictionaries
download_requested = pyqtSignal(list)
def __init__(self, links_data, parent_app, parent=None):
@@ -34,23 +27,13 @@ class DownloadExtractedLinksDialog(QDialog):
super().__init__(parent)
self.links_data = links_data
self.parent_app = parent_app
-
- # --- Basic Window Setup ---
app_icon = get_app_icon_object()
if not app_icon.isNull():
self.setWindowIcon(app_icon)
-
- # --- START OF FIX ---
- # Get the user-defined scale factor from the parent application.
scale_factor = getattr(self.parent_app, 'scale_factor', 1.0)
-
- # Define base dimensions and apply the correct scale factor.
base_width, base_height = 600, 450
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))
- # --- END OF FIX ---
-
- # --- Initialize UI and Apply Theming ---
self._init_ui()
self._retranslate_ui()
self._apply_theme()
@@ -68,8 +51,6 @@ class DownloadExtractedLinksDialog(QDialog):
self.links_list_widget.setSelectionMode(QAbstractItemView.NoSelection)
self._populate_list()
layout.addWidget(self.links_list_widget)
-
- # --- Control Buttons ---
button_layout = QHBoxLayout()
self.select_all_button = QPushButton()
self.select_all_button.clicked.connect(lambda: self._set_all_items_checked(Qt.Checked))
@@ -100,7 +81,6 @@ class DownloadExtractedLinksDialog(QDialog):
sorted_post_titles = sorted(grouped_links.keys(), key=lambda x: x.lower())
for post_title_key in sorted_post_titles:
- # Add a non-selectable header for each post
header_item = QListWidgetItem(f"{post_title_key}")
header_item.setFlags(Qt.NoItemFlags)
font = header_item.font()
@@ -108,8 +88,6 @@ class DownloadExtractedLinksDialog(QDialog):
font.setPointSize(font.pointSize() + 1)
header_item.setFont(font)
self.links_list_widget.addItem(header_item)
-
- # Add checkable items for each link within that post
for link_info_data in grouped_links[post_title_key]:
platform_display = link_info_data.get('platform', 'unknown').upper()
display_text = f" [{platform_display}] {link_info_data['link_text']} ({link_info_data['url']})"
@@ -139,19 +117,13 @@ class DownloadExtractedLinksDialog(QDialog):
is_dark_theme = self.parent_app and self.parent_app.current_theme == "dark"
if is_dark_theme:
- # Get the scale factor from the parent app
scale = getattr(self.parent_app, 'scale_factor', 1)
- # Call the imported function with the correct scale
self.setStyleSheet(get_dark_theme(scale))
else:
- # Explicitly set a blank stylesheet for light mode
self.setStyleSheet("")
-
- # Set header text color based on theme
header_color = Qt.cyan if is_dark_theme else Qt.blue
for i in range(self.links_list_widget.count()):
item = self.links_list_widget.item(i)
- # Headers are not checkable (they have no checkable flag)
if not item.flags() & Qt.ItemIsUserCheckable:
item.setForeground(header_color)
diff --git a/src/ui/dialogs/DownloadHistoryDialog.py b/src/ui/dialogs/DownloadHistoryDialog.py
index 7855fe8..68e83a6 100644
--- a/src/ui/dialogs/DownloadHistoryDialog.py
+++ b/src/ui/dialogs/DownloadHistoryDialog.py
@@ -1,16 +1,12 @@
-# --- Standard Library Imports ---
import os
import time
import json
-# --- PyQt5 Imports ---
from PyQt5.QtCore import Qt, QStandardPaths, QTimer
from PyQt5.QtWidgets import (
QApplication, QDialog, QHBoxLayout, QLabel, QScrollArea,
QPushButton, QVBoxLayout, QSplitter, QWidget, QGroupBox,
QFileDialog, QMessageBox
)
-
-# --- Local Application Imports ---
from ...i18n.translator import get_translation
from ..main_window import get_app_icon_object
from ...utils.resolution import get_dark_theme
@@ -25,17 +21,14 @@ class DownloadHistoryDialog (QDialog ):
self .first_processed_entries =first_processed_entries
self .setModal (True )
self._apply_theme()
- # Patch missing creator_display_name and creator_name using parent_app.creator_name_cache if available
creator_name_cache = getattr(parent_app, 'creator_name_cache', None)
if creator_name_cache:
- # Patch left pane (files)
for entry in self.last_3_downloaded_entries:
if not entry.get('creator_display_name'):
service = entry.get('service', '').lower()
user_id = str(entry.get('user_id', ''))
key = (service, user_id)
entry['creator_display_name'] = creator_name_cache.get(key, entry.get('folder_context_name', 'Unknown Creator/Series'))
- # Patch right pane (posts)
for entry in self.first_processed_entries:
if not entry.get('creator_name'):
service = entry.get('service', '').lower()
diff --git a/src/ui/dialogs/ErrorFilesDialog.py b/src/ui/dialogs/ErrorFilesDialog.py
index 2fc6a47..899412c 100644
--- a/src/ui/dialogs/ErrorFilesDialog.py
+++ b/src/ui/dialogs/ErrorFilesDialog.py
@@ -42,11 +42,15 @@ class ErrorFilesDialog(QDialog):
if app_icon and not app_icon.isNull():
self.setWindowIcon(app_icon)
+ # --- START OF FIX ---
+ # Get the user-defined scale factor from the parent application.
scale_factor = getattr(self.parent_app, 'scale_factor', 1.0)
+ # Define base dimensions and apply the correct scale factor.
base_width, base_height = 550, 400
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))
+ # --- END OF FIX ---
# --- Initialize UI and Apply Theming ---
self._init_ui()
diff --git a/src/ui/dialogs/FavoritePostsDialog.py b/src/ui/dialogs/FavoritePostsDialog.py
index df0210e..5decc0a 100644
--- a/src/ui/dialogs/FavoritePostsDialog.py
+++ b/src/ui/dialogs/FavoritePostsDialog.py
@@ -1,4 +1,3 @@
-# --- Standard Library Imports ---
import html
import os
import sys
@@ -8,8 +7,6 @@ import traceback
import json
import re
from collections import defaultdict
-
-# --- Third-Party Library Imports ---
import requests
from PyQt5.QtCore import QCoreApplication, Qt, pyqtSignal, QThread
from PyQt5.QtWidgets import (
@@ -17,12 +14,9 @@ from PyQt5.QtWidgets import (
QListWidgetItem, QMessageBox, QPushButton, QVBoxLayout, QProgressBar,
QWidget, QCheckBox
)
-
-# --- Local Application Imports ---
from ...i18n.translator import get_translation
from ..assets import get_app_icon_object
from ...utils.network_utils import prepare_cookies_for_request
-# Corrected Import: Import CookieHelpDialog directly from its own module
from .CookieHelpDialog import CookieHelpDialog
from ...core.api_client import download_from_api
from ...utils.resolution import get_dark_theme
diff --git a/src/ui/dialogs/HelpGuideDialog.py b/src/ui/dialogs/HelpGuideDialog.py
index b927371..7fb196a 100644
--- a/src/ui/dialogs/HelpGuideDialog.py
+++ b/src/ui/dialogs/HelpGuideDialog.py
@@ -1,16 +1,11 @@
-# --- Standard Library Imports ---
import os
import sys
-
-# --- PyQt5 Imports ---
from PyQt5.QtCore import QUrl, QSize, Qt
from PyQt5.QtGui import QIcon, QDesktopServices
from PyQt5.QtWidgets import (
QApplication, QDialog, QHBoxLayout, QLabel, QPushButton, QVBoxLayout,
QStackedWidget, QScrollArea, QFrame, QWidget
)
-
-# --- Local Application Imports ---
from ...i18n.translator import get_translation
from ..main_window import get_app_icon_object
from ...utils.resolution import get_dark_theme
diff --git a/src/ui/dialogs/KeepDuplicatesDialog.py b/src/ui/dialogs/KeepDuplicatesDialog.py
index ec8bf7f..38fb8cd 100644
--- a/src/ui/dialogs/KeepDuplicatesDialog.py
+++ b/src/ui/dialogs/KeepDuplicatesDialog.py
@@ -1,13 +1,8 @@
-# KeepDuplicatesDialog.py
-
-# --- PyQt5 Imports ---
from PyQt5.QtWidgets import (
QDialog, QVBoxLayout, QGroupBox, QRadioButton,
QPushButton, QHBoxLayout, QButtonGroup, QLabel, QLineEdit
)
from PyQt5.QtGui import QIntValidator
-
-# --- Local Application Imports ---
from ...i18n.translator import get_translation
from ...config.constants import DUPLICATE_HANDLING_HASH, DUPLICATE_HANDLING_KEEP_ALL
@@ -25,8 +20,6 @@ class KeepDuplicatesDialog(QDialog):
if self.parent_app and hasattr(self.parent_app, '_apply_theme_to_widget'):
self.parent_app._apply_theme_to_widget(self)
-
- # Set the initial state based on current settings
if current_mode == DUPLICATE_HANDLING_KEEP_ALL:
self.radio_keep_everything.setChecked(True)
self.limit_input.setText(str(current_limit) if current_limit > 0 else "")
@@ -44,13 +37,9 @@ class KeepDuplicatesDialog(QDialog):
options_group = QGroupBox()
options_layout = QVBoxLayout(options_group)
self.button_group = QButtonGroup(self)
-
- # --- Skip by Hash Option ---
self.radio_skip_by_hash = QRadioButton()
self.button_group.addButton(self.radio_skip_by_hash)
options_layout.addWidget(self.radio_skip_by_hash)
-
- # --- Keep Everything Option with Limit Input ---
keep_everything_layout = QHBoxLayout()
self.radio_keep_everything = QRadioButton()
self.button_group.addButton(self.radio_keep_everything)
@@ -66,8 +55,6 @@ class KeepDuplicatesDialog(QDialog):
options_layout.addLayout(keep_everything_layout)
main_layout.addWidget(options_group)
-
- # --- OK and Cancel buttons ---
button_layout = QHBoxLayout()
self.ok_button = QPushButton()
self.cancel_button = QPushButton()
@@ -75,8 +62,6 @@ class KeepDuplicatesDialog(QDialog):
button_layout.addWidget(self.ok_button)
button_layout.addWidget(self.cancel_button)
main_layout.addLayout(button_layout)
-
- # --- Connections ---
self.ok_button.clicked.connect(self.accept)
self.cancel_button.clicked.connect(self.reject)
self.radio_keep_everything.toggled.connect(self.limit_input.setEnabled)
diff --git a/src/ui/dialogs/KnownNamesFilterDialog.py b/src/ui/dialogs/KnownNamesFilterDialog.py
index fd37b74..8b4ac6b 100644
--- a/src/ui/dialogs/KnownNamesFilterDialog.py
+++ b/src/ui/dialogs/KnownNamesFilterDialog.py
@@ -37,12 +37,16 @@ class KnownNamesFilterDialog(QDialog):
if app_icon and not app_icon.isNull():
self.setWindowIcon(app_icon)
- # Set window size dynamically
- screen_geometry = QApplication.primaryScreen().availableGeometry()
+ # --- START OF FIX ---
+ # Get the user-defined scale factor from the parent application
+ # instead of calculating an independent one.
scale_factor = getattr(self.parent_app, 'scale_factor', 1.0)
+
+ # Define base size and apply the correct scale factor
base_width, base_height = 460, 450
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))
+ # --- END OF FIX ---
# --- Initialize UI and Apply Theming ---
self._init_ui()
diff --git a/src/ui/dialogs/SinglePDF.py b/src/ui/dialogs/SinglePDF.py
index 41785e9..7bef00c 100644
--- a/src/ui/dialogs/SinglePDF.py
+++ b/src/ui/dialogs/SinglePDF.py
@@ -1,34 +1,33 @@
-# SinglePDF.py
-
import os
+import re
try:
from fpdf import FPDF
FPDF_AVAILABLE = True
except ImportError:
FPDF_AVAILABLE = False
+def strip_html_tags(text):
+ if not text:
+ return ""
+ clean = re.compile('<.*?>')
+ return re.sub(clean, '', text)
+
class PDF(FPDF):
"""Custom PDF class to handle headers and footers."""
def header(self):
- # No header
- pass
+ pass
def footer(self):
- # Position at 1.5 cm from bottom
self.set_y(-15)
- self.set_font('DejaVu', '', 8)
- # Page number
+ if self.font_family:
+ self.set_font(self.font_family, '', 8)
+ else:
+ self.set_font('Arial', '', 8)
self.cell(0, 10, 'Page ' + str(self.page_no()), 0, 0, 'C')
def create_single_pdf_from_content(posts_data, output_filename, font_path, logger=print):
"""
- Creates a single PDF from a list of post titles and content.
-
- Args:
- posts_data (list): A list of dictionaries, where each dict has 'title' and 'content' keys.
- output_filename (str): The full path for the output PDF file.
- font_path (str): Path to the DejaVuSans.ttf font file.
- logger (function, optional): A function to log progress and errors. Defaults to print.
+ Creates a single, continuous PDF, correctly formatting both descriptions and comments.
"""
if not FPDF_AVAILABLE:
logger("❌ PDF Creation failed: 'fpdf2' library is not installed. Please run: pip install fpdf2")
@@ -39,34 +38,66 @@ def create_single_pdf_from_content(posts_data, output_filename, font_path, logge
return False
pdf = PDF()
+ default_font_family = 'DejaVu'
+ bold_font_path = ""
+ if font_path:
+ bold_font_path = font_path.replace("DejaVuSans.ttf", "DejaVuSans-Bold.ttf")
+
try:
- if not os.path.exists(font_path):
- raise RuntimeError("Font file not found.")
- pdf.add_font('DejaVu', '', font_path, uni=True)
- pdf.add_font('DejaVu', 'B', font_path, uni=True) # Add Bold variant
- except Exception as font_error:
- logger(f" ⚠️ Could not load DejaVu font: {font_error}")
- logger(" PDF may not support all characters. Falling back to default Arial font.")
- pdf.set_font('Arial', '', 12)
- pdf.set_font('Arial', 'B', 16)
-
- logger(f" Starting PDF creation with content from {len(posts_data)} posts...")
-
- for post in posts_data:
- pdf.add_page()
- # Post Title
- pdf.set_font('DejaVu', 'B', 16)
-
- # vvv THIS LINE IS CORRECTED vvv
- # We explicitly set align='L' and remove the incorrect positional arguments.
- pdf.multi_cell(w=0, h=10, text=post.get('title', 'Untitled Post'), align='L')
+ if not os.path.exists(font_path): raise RuntimeError(f"Font file not found: {font_path}")
+ if not os.path.exists(bold_font_path): raise RuntimeError(f"Bold font file not found: {bold_font_path}")
- pdf.ln(5) # Add a little space after the title
+ pdf.add_font('DejaVu', '', font_path, uni=True)
+ pdf.add_font('DejaVu', 'B', bold_font_path, uni=True)
+ except Exception as font_error:
+ logger(f" ⚠️ Could not load DejaVu font: {font_error}. Falling back to Arial.")
+ default_font_family = 'Arial'
+
+ pdf.add_page()
- # Post Content
- pdf.set_font('DejaVu', '', 12)
- pdf.multi_cell(w=0, h=7, text=post.get('content', 'No Content'))
+ logger(f" Starting continuous PDF creation with content from {len(posts_data)} posts...")
+
+ for i, post in enumerate(posts_data):
+ if i > 0:
+ if 'content' in post:
+ pdf.add_page()
+ elif 'comments' in post:
+ pdf.ln(10)
+ pdf.cell(0, 0, '', border='T')
+ pdf.ln(10)
+
+ pdf.set_font(default_font_family, 'B', 16)
+ pdf.multi_cell(w=0, h=10, text=post.get('title', 'Untitled Post'), align='L')
+ pdf.ln(5)
+
+ if 'comments' in post and post['comments']:
+ comments_list = post['comments']
+ for comment_index, comment in enumerate(comments_list):
+ user = comment.get('commenter_name', 'Unknown User')
+ timestamp = comment.get('published', 'No Date')
+ body = strip_html_tags(comment.get('content', ''))
+
+ pdf.set_font(default_font_family, '', 10)
+ pdf.write(8, "Comment by: ")
+ if user is not None:
+ pdf.set_font(default_font_family, 'B', 10)
+ pdf.write(8, str(user))
+
+ pdf.set_font(default_font_family, '', 10)
+ pdf.write(8, f" on {timestamp}")
+ pdf.ln(10)
+
+ pdf.set_font(default_font_family, '', 11)
+ pdf.multi_cell(0, 7, body)
+
+ if comment_index < len(comments_list) - 1:
+ pdf.ln(3)
+ pdf.cell(w=0, h=0, border='T')
+ pdf.ln(3)
+ elif 'content' in post:
+ pdf.set_font(default_font_family, '', 12)
+ pdf.multi_cell(w=0, h=7, text=post.get('content', 'No Content'))
try:
pdf.output(output_filename)
diff --git a/src/ui/dialogs/TourDialog.py b/src/ui/dialogs/TourDialog.py
index b40aeea..c0c7fce 100644
--- a/src/ui/dialogs/TourDialog.py
+++ b/src/ui/dialogs/TourDialog.py
@@ -1,15 +1,10 @@
-# --- Standard Library Imports ---
import os
import sys
-
-# --- PyQt5 Imports ---
from PyQt5.QtCore import pyqtSignal, Qt, QSettings, QCoreApplication
from PyQt5.QtWidgets import (
QApplication, QDialog, QHBoxLayout, QLabel, QPushButton, QVBoxLayout,
QStackedWidget, QScrollArea, QFrame, QWidget, QCheckBox
)
-
-# --- Local Application Imports ---
from ...i18n.translator import get_translation
from ..main_window import get_app_icon_object
from ...utils.resolution import get_dark_theme
@@ -58,8 +53,6 @@ class TourDialog(QDialog):
"""
tour_finished_normally = pyqtSignal()
tour_skipped = pyqtSignal()
-
- # Constants for QSettings
CONFIG_APP_NAME_TOUR = "ApplicationTour"
TOUR_SHOWN_KEY = "neverShowTourAgainV19"
@@ -98,8 +91,6 @@ class TourDialog(QDialog):
self.stacked_widget = QStackedWidget()
main_layout.addWidget(self.stacked_widget, 1)
-
- # Load content for each step
steps_content = [
("tour_dialog_step1_title", "tour_dialog_step1_content"),
("tour_dialog_step2_title", "tour_dialog_step2_content"),
@@ -120,8 +111,6 @@ class TourDialog(QDialog):
self.stacked_widget.addWidget(step_widget)
self.setWindowTitle(self._tr("tour_dialog_title", "Welcome to Kemono Downloader!"))
-
- # --- Bottom Controls ---
bottom_controls_layout = QVBoxLayout()
bottom_controls_layout.setContentsMargins(15, 10, 15, 15)
bottom_controls_layout.setSpacing(12)
diff --git a/src/ui/main_window.py b/src/ui/main_window.py
index 3a1c69c..9d766de 100644
--- a/src/ui/main_window.py
+++ b/src/ui/main_window.py
@@ -1,4 +1,3 @@
-# --- Standard Library Imports ---
import sys
import os
import time
@@ -16,8 +15,6 @@ from collections import deque, defaultdict
import threading
from concurrent.futures import Future, ThreadPoolExecutor ,CancelledError
from urllib .parse import urlparse
-
-# --- PyQt5 Imports ---
from PyQt5.QtGui import QIcon, QIntValidator, QDesktopServices
from PyQt5.QtWidgets import (
QApplication, QWidget, QLabel, QLineEdit, QTextEdit, QPushButton,
@@ -27,8 +24,6 @@ from PyQt5.QtWidgets import (
QMainWindow, QAction, QGridLayout,
)
from PyQt5.QtCore import Qt, QThread, pyqtSignal, QObject, QTimer, QSettings, QStandardPaths, QUrl, QSize, QProcess, QMutex, QMutexLocker, QCoreApplication
-
-# --- Local Application Imports ---
from ..services.drive_downloader import download_mega_file as drive_download_mega_file ,download_gdrive_file ,download_dropbox_file
from ..core.workers import DownloadThread as BackendDownloadThread
from ..core.workers import PostProcessorWorker
@@ -137,8 +132,6 @@ class DownloaderApp (QWidget ):
self.creator_name_cache = {}
self.log_signal.emit(f"ℹ️ App base directory: {self.app_base_dir}")
self.log_signal.emit(f"ℹ️ Persistent history file path set to: {self.persistent_history_file}")
-
- # --- The rest of your __init__ method continues from here ---
self.last_downloaded_files_details = deque(maxlen=3)
self.download_history_candidates = deque(maxlen=8)
self.final_download_history_entries = []
@@ -225,7 +218,7 @@ class DownloaderApp (QWidget ):
self.text_export_format = 'pdf'
self.single_pdf_setting = False
self.keep_duplicates_mode = DUPLICATE_HANDLING_HASH
- self.keep_duplicates_limit = 0 # 0 means no limit
+ self.keep_duplicates_limit = 0
self.downloaded_hash_counts = defaultdict(int)
self.downloaded_hash_counts_lock = threading.Lock()
self.session_temp_files = []
@@ -288,8 +281,6 @@ class DownloaderApp (QWidget ):
self.setStyleSheet(get_dark_theme(scale))
else:
self.setStyleSheet("")
-
- # Prompt for restart
msg_box = QMessageBox(self)
msg_box.setIcon(QMessageBox.Information)
msg_box.setWindowTitle(self._tr("theme_change_title", "Theme Changed"))
@@ -447,14 +438,14 @@ class DownloaderApp (QWidget ):
self._load_ui_from_settings_dict(settings)
self.is_restore_pending = True
- self._update_button_states_and_connections() # Update buttons for restore state, UI remains editable
+ self._update_button_states_and_connections()
def _clear_session_and_reset_ui(self):
"""Clears the session file and resets the UI to its default state."""
self._clear_session_file()
self.interrupted_session_data = None
self.is_restore_pending = False
- self._update_button_states_and_connections() # Ensure buttons are updated to idle state
+ self._update_button_states_and_connections()
self.reset_application_state()
def _clear_session_file(self):
@@ -489,7 +480,6 @@ class DownloaderApp (QWidget ):
Updates the text and click connections of the main action buttons
based on the current application state (downloading, paused, restore pending, idle).
"""
- # Disconnect all signals first to prevent multiple connections
try: self.download_btn.clicked.disconnect()
except TypeError: pass
try: self.pause_btn.clicked.disconnect()
@@ -500,7 +490,6 @@ class DownloaderApp (QWidget ):
is_download_active = self._is_download_active()
if self.is_restore_pending:
- # State: Restore Pending
self.download_btn.setText(self._tr("start_download_button_text", "⬇️ Start Download"))
self.download_btn.setEnabled(True)
self.download_btn.clicked.connect(self.start_download)
@@ -511,14 +500,12 @@ class DownloaderApp (QWidget ):
self.pause_btn.clicked.connect(self.restore_download)
self.pause_btn.setToolTip(self._tr("restore_download_button_tooltip", "Click to restore the interrupted download."))
- # --- START: CORRECTED CANCEL BUTTON LOGIC ---
self.cancel_btn.setText(self._tr("discard_session_button_text", "🗑️ Discard Session"))
self.cancel_btn.setEnabled(True)
self.cancel_btn.clicked.connect(self._clear_session_and_reset_ui)
self.cancel_btn.setToolTip(self._tr("discard_session_tooltip", "Click to discard the interrupted session and reset the UI."))
elif is_download_active:
- # State: Downloading / Paused
self.download_btn.setText(self._tr("start_download_button_text", "⬇️ Start Download"))
self.download_btn.setEnabled(False) # Cannot start new download while one is active
@@ -532,7 +519,6 @@ class DownloaderApp (QWidget ):
self.cancel_btn.clicked.connect(self.cancel_download_button_action)
self.cancel_btn.setToolTip(self._tr("cancel_button_tooltip", "Click to cancel the ongoing download/extraction process and reset the UI fields (preserving URL and Directory)."))
else:
- # State: Idle (No download, no restore pending)
self.download_btn.setText(self._tr("start_download_button_text", "⬇️ Start Download"))
self.download_btn.setEnabled(True)
self.download_btn.clicked.connect(self.start_download)
@@ -870,7 +856,7 @@ class DownloaderApp (QWidget ):
self ._handle_actual_file_downloaded (payload [0 ]if payload else {})
elif signal_type =='file_successfully_downloaded':
self ._handle_file_successfully_downloaded (payload [0 ])
- elif signal_type == 'worker_finished': # <-- ADD THIS ELIF BLOCK
+ elif signal_type == 'worker_finished':
self.actual_gui_signals.worker_finished_signal.emit(payload[0] if payload else tuple())
else:
self .log_signal .emit (f"⚠️ Unknown signal type from worker queue: {signal_type }")
@@ -1344,13 +1330,7 @@ class DownloaderApp (QWidget ):
def _show_future_settings_dialog(self):
"""Shows the placeholder dialog for future settings."""
- # --- DEBUGGING CODE TO FIND THE UNEXPECTED CALL ---
- import traceback
- print("--- DEBUG: _show_future_settings_dialog() was called. See stack trace below. ---")
- traceback.print_stack()
- print("--------------------------------------------------------------------------------")
- # Correctly create the dialog instance once with the parent set to self.
dialog = FutureSettingsDialog(self)
dialog.exec_()
@@ -1364,7 +1344,6 @@ class DownloaderApp (QWidget ):
Checks if the fetcher thread is done AND if all submitted tasks have been processed.
If so, finalizes the download.
"""
- # Conditions for being completely finished:
fetcher_is_done = not self.is_fetcher_thread_running
all_workers_are_done = (self.total_posts_to_process > 0 and self.processed_posts_count >= self.total_posts_to_process)
@@ -1643,24 +1622,15 @@ class DownloaderApp (QWidget ):
is_only_links_mode =self .radio_only_links and self .radio_only_links .isChecked ()
if is_only_links_mode:
- # Check if this is a new post title
if post_title != self._current_link_post_title:
- # Add a styled horizontal rule as a separator
if self._current_link_post_title is not None:
separator_html = f'{HTML_PREFIX}